1from datetime import date, datetime, time as dt_time, timedelta
2from typing import Dict, List, Optional, Tuple, Type
3
4from dateutil.tz import tzlocal
5import numpy as np
6import pytest
7
8from pandas._libs.tslibs import (
9    NaT,
10    OutOfBoundsDatetime,
11    Timestamp,
12    conversion,
13    timezones,
14)
15import pandas._libs.tslibs.offsets as liboffsets
16from pandas._libs.tslibs.offsets import ApplyTypeError, _get_offset, _offset_map
17from pandas._libs.tslibs.period import INVALID_FREQ_ERR_MSG
18from pandas.compat import IS64
19from pandas.compat.numpy import np_datetime64_compat
20from pandas.errors import PerformanceWarning
21
22import pandas._testing as tm
23from pandas.core.indexes.datetimes import DatetimeIndex, date_range
24from pandas.core.series import Series
25
26from pandas.io.pickle import read_pickle
27from pandas.tseries.holiday import USFederalHolidayCalendar
28import pandas.tseries.offsets as offsets
29from pandas.tseries.offsets import (
30    FY5253,
31    BaseOffset,
32    BDay,
33    BMonthBegin,
34    BMonthEnd,
35    BQuarterBegin,
36    BQuarterEnd,
37    BusinessHour,
38    BYearBegin,
39    BYearEnd,
40    CBMonthBegin,
41    CBMonthEnd,
42    CDay,
43    CustomBusinessDay,
44    CustomBusinessHour,
45    CustomBusinessMonthBegin,
46    CustomBusinessMonthEnd,
47    DateOffset,
48    Day,
49    Easter,
50    FY5253Quarter,
51    LastWeekOfMonth,
52    MonthBegin,
53    MonthEnd,
54    Nano,
55    QuarterBegin,
56    QuarterEnd,
57    SemiMonthBegin,
58    SemiMonthEnd,
59    Tick,
60    Week,
61    WeekOfMonth,
62    YearBegin,
63    YearEnd,
64)
65
66from .common import assert_is_on_offset, assert_offset_equal
67
68
69class WeekDay:
70    # TODO: Remove: This is not used outside of tests
71    MON = 0
72    TUE = 1
73    WED = 2
74    THU = 3
75    FRI = 4
76    SAT = 5
77    SUN = 6
78
79
80#####
81# DateOffset Tests
82#####
83_ApplyCases = List[Tuple[BaseOffset, Dict[datetime, datetime]]]
84
85
86class Base:
87    _offset: Optional[Type[DateOffset]] = None
88    d = Timestamp(datetime(2008, 1, 2))
89
90    timezones = [
91        None,
92        "UTC",
93        "Asia/Tokyo",
94        "US/Eastern",
95        "dateutil/Asia/Tokyo",
96        "dateutil/US/Pacific",
97    ]
98
99    def _get_offset(self, klass, value=1, normalize=False):
100        # create instance from offset class
101        if klass is FY5253:
102            klass = klass(
103                n=value,
104                startingMonth=1,
105                weekday=1,
106                variation="last",
107                normalize=normalize,
108            )
109        elif klass is FY5253Quarter:
110            klass = klass(
111                n=value,
112                startingMonth=1,
113                weekday=1,
114                qtr_with_extra_week=1,
115                variation="last",
116                normalize=normalize,
117            )
118        elif klass is LastWeekOfMonth:
119            klass = klass(n=value, weekday=5, normalize=normalize)
120        elif klass is WeekOfMonth:
121            klass = klass(n=value, week=1, weekday=5, normalize=normalize)
122        elif klass is Week:
123            klass = klass(n=value, weekday=5, normalize=normalize)
124        elif klass is DateOffset:
125            klass = klass(days=value, normalize=normalize)
126        else:
127            klass = klass(value, normalize=normalize)
128        return klass
129
130    def test_apply_out_of_range(self, tz_naive_fixture):
131        tz = tz_naive_fixture
132        if self._offset is None:
133            return
134        if isinstance(tz, tzlocal) and not IS64:
135            pytest.xfail(reason="OverflowError inside tzlocal past 2038")
136
137        # try to create an out-of-bounds result timestamp; if we can't create
138        # the offset skip
139        try:
140            if self._offset in (BusinessHour, CustomBusinessHour):
141                # Using 10000 in BusinessHour fails in tz check because of DST
142                # difference
143                offset = self._get_offset(self._offset, value=100000)
144            else:
145                offset = self._get_offset(self._offset, value=10000)
146
147            result = Timestamp("20080101") + offset
148            assert isinstance(result, datetime)
149            assert result.tzinfo is None
150
151            # Check tz is preserved
152            t = Timestamp("20080101", tz=tz)
153            result = t + offset
154            assert isinstance(result, datetime)
155            assert t.tzinfo == result.tzinfo
156
157        except OutOfBoundsDatetime:
158            pass
159        except (ValueError, KeyError):
160            # we are creating an invalid offset
161            # so ignore
162            pass
163
164    def test_offsets_compare_equal(self):
165        # root cause of GH#456: __ne__ was not implemented
166        if self._offset is None:
167            return
168        offset1 = self._offset()
169        offset2 = self._offset()
170        assert not offset1 != offset2
171        assert offset1 == offset2
172
173    def test_rsub(self):
174        if self._offset is None or not hasattr(self, "offset2"):
175            # i.e. skip for TestCommon and YQM subclasses that do not have
176            # offset2 attr
177            return
178        assert self.d - self.offset2 == (-self.offset2).apply(self.d)
179
180    def test_radd(self):
181        if self._offset is None or not hasattr(self, "offset2"):
182            # i.e. skip for TestCommon and YQM subclasses that do not have
183            # offset2 attr
184            return
185        assert self.d + self.offset2 == self.offset2 + self.d
186
187    def test_sub(self):
188        if self._offset is None or not hasattr(self, "offset2"):
189            # i.e. skip for TestCommon and YQM subclasses that do not have
190            # offset2 attr
191            return
192        off = self.offset2
193        msg = "Cannot subtract datetime from offset"
194        with pytest.raises(TypeError, match=msg):
195            off - self.d
196
197        assert 2 * off - off == off
198        assert self.d - self.offset2 == self.d + self._offset(-2)
199        assert self.d - self.offset2 == self.d - (2 * off - off)
200
201    def testMult1(self):
202        if self._offset is None or not hasattr(self, "offset1"):
203            # i.e. skip for TestCommon and YQM subclasses that do not have
204            # offset1 attr
205            return
206        assert self.d + 10 * self.offset1 == self.d + self._offset(10)
207        assert self.d + 5 * self.offset1 == self.d + self._offset(5)
208
209    def testMult2(self):
210        if self._offset is None:
211            return
212        assert self.d + (-5 * self._offset(-10)) == self.d + self._offset(50)
213        assert self.d + (-3 * self._offset(-2)) == self.d + self._offset(6)
214
215    def test_compare_str(self):
216        # GH#23524
217        # comparing to strings that cannot be cast to DateOffsets should
218        #  not raise for __eq__ or __ne__
219        if self._offset is None:
220            return
221        off = self._get_offset(self._offset)
222
223        assert not off == "infer"
224        assert off != "foo"
225        # Note: inequalities are only implemented for Tick subclasses;
226        #  tests for this are in test_ticks
227
228
229class TestCommon(Base):
230    # exected value created by Base._get_offset
231    # are applied to 2011/01/01 09:00 (Saturday)
232    # used for .apply and .rollforward
233    expecteds = {
234        "Day": Timestamp("2011-01-02 09:00:00"),
235        "DateOffset": Timestamp("2011-01-02 09:00:00"),
236        "BusinessDay": Timestamp("2011-01-03 09:00:00"),
237        "CustomBusinessDay": Timestamp("2011-01-03 09:00:00"),
238        "CustomBusinessMonthEnd": Timestamp("2011-01-31 09:00:00"),
239        "CustomBusinessMonthBegin": Timestamp("2011-01-03 09:00:00"),
240        "MonthBegin": Timestamp("2011-02-01 09:00:00"),
241        "BusinessMonthBegin": Timestamp("2011-01-03 09:00:00"),
242        "MonthEnd": Timestamp("2011-01-31 09:00:00"),
243        "SemiMonthEnd": Timestamp("2011-01-15 09:00:00"),
244        "SemiMonthBegin": Timestamp("2011-01-15 09:00:00"),
245        "BusinessMonthEnd": Timestamp("2011-01-31 09:00:00"),
246        "YearBegin": Timestamp("2012-01-01 09:00:00"),
247        "BYearBegin": Timestamp("2011-01-03 09:00:00"),
248        "YearEnd": Timestamp("2011-12-31 09:00:00"),
249        "BYearEnd": Timestamp("2011-12-30 09:00:00"),
250        "QuarterBegin": Timestamp("2011-03-01 09:00:00"),
251        "BQuarterBegin": Timestamp("2011-03-01 09:00:00"),
252        "QuarterEnd": Timestamp("2011-03-31 09:00:00"),
253        "BQuarterEnd": Timestamp("2011-03-31 09:00:00"),
254        "BusinessHour": Timestamp("2011-01-03 10:00:00"),
255        "CustomBusinessHour": Timestamp("2011-01-03 10:00:00"),
256        "WeekOfMonth": Timestamp("2011-01-08 09:00:00"),
257        "LastWeekOfMonth": Timestamp("2011-01-29 09:00:00"),
258        "FY5253Quarter": Timestamp("2011-01-25 09:00:00"),
259        "FY5253": Timestamp("2011-01-25 09:00:00"),
260        "Week": Timestamp("2011-01-08 09:00:00"),
261        "Easter": Timestamp("2011-04-24 09:00:00"),
262        "Hour": Timestamp("2011-01-01 10:00:00"),
263        "Minute": Timestamp("2011-01-01 09:01:00"),
264        "Second": Timestamp("2011-01-01 09:00:01"),
265        "Milli": Timestamp("2011-01-01 09:00:00.001000"),
266        "Micro": Timestamp("2011-01-01 09:00:00.000001"),
267        "Nano": Timestamp(np_datetime64_compat("2011-01-01T09:00:00.000000001Z")),
268    }
269
270    def test_immutable(self, offset_types):
271        # GH#21341 check that __setattr__ raises
272        offset = self._get_offset(offset_types)
273        msg = "objects is not writable|DateOffset objects are immutable"
274        with pytest.raises(AttributeError, match=msg):
275            offset.normalize = True
276        with pytest.raises(AttributeError, match=msg):
277            offset.n = 91
278
279    def test_return_type(self, offset_types):
280        offset = self._get_offset(offset_types)
281
282        # make sure that we are returning a Timestamp
283        result = Timestamp("20080101") + offset
284        assert isinstance(result, Timestamp)
285
286        # make sure that we are returning NaT
287        assert NaT + offset is NaT
288        assert offset + NaT is NaT
289
290        assert NaT - offset is NaT
291        assert (-offset).apply(NaT) is NaT
292
293    def test_offset_n(self, offset_types):
294        offset = self._get_offset(offset_types)
295        assert offset.n == 1
296
297        neg_offset = offset * -1
298        assert neg_offset.n == -1
299
300        mul_offset = offset * 3
301        assert mul_offset.n == 3
302
303    def test_offset_timedelta64_arg(self, offset_types):
304        # check that offset._validate_n raises TypeError on a timedelt64
305        #  object
306        off = self._get_offset(offset_types)
307
308        td64 = np.timedelta64(4567, "s")
309        with pytest.raises(TypeError, match="argument must be an integer"):
310            type(off)(n=td64, **off.kwds)
311
312    def test_offset_mul_ndarray(self, offset_types):
313        off = self._get_offset(offset_types)
314
315        expected = np.array([[off, off * 2], [off * 3, off * 4]])
316
317        result = np.array([[1, 2], [3, 4]]) * off
318        tm.assert_numpy_array_equal(result, expected)
319
320        result = off * np.array([[1, 2], [3, 4]])
321        tm.assert_numpy_array_equal(result, expected)
322
323    def test_offset_freqstr(self, offset_types):
324        offset = self._get_offset(offset_types)
325
326        freqstr = offset.freqstr
327        if freqstr not in ("<Easter>", "<DateOffset: days=1>", "LWOM-SAT"):
328            code = _get_offset(freqstr)
329            assert offset.rule_code == code
330
331    def _check_offsetfunc_works(self, offset, funcname, dt, expected, normalize=False):
332
333        if normalize and issubclass(offset, Tick):
334            # normalize=True disallowed for Tick subclasses GH#21427
335            return
336
337        offset_s = self._get_offset(offset, normalize=normalize)
338        func = getattr(offset_s, funcname)
339
340        result = func(dt)
341        assert isinstance(result, Timestamp)
342        assert result == expected
343
344        result = func(Timestamp(dt))
345        assert isinstance(result, Timestamp)
346        assert result == expected
347
348        # see gh-14101
349        exp_warning = None
350        ts = Timestamp(dt) + Nano(5)
351
352        if (
353            type(offset_s).__name__ == "DateOffset"
354            and (funcname == "apply" or normalize)
355            and ts.nanosecond > 0
356        ):
357            exp_warning = UserWarning
358
359        # test nanosecond is preserved
360        with tm.assert_produces_warning(exp_warning, check_stacklevel=False):
361            result = func(ts)
362        assert isinstance(result, Timestamp)
363        if normalize is False:
364            assert result == expected + Nano(5)
365        else:
366            assert result == expected
367
368        if isinstance(dt, np.datetime64):
369            # test tz when input is datetime or Timestamp
370            return
371
372        for tz in self.timezones:
373            expected_localize = expected.tz_localize(tz)
374            tz_obj = timezones.maybe_get_tz(tz)
375            dt_tz = conversion.localize_pydatetime(dt, tz_obj)
376
377            result = func(dt_tz)
378            assert isinstance(result, Timestamp)
379            assert result == expected_localize
380
381            result = func(Timestamp(dt, tz=tz))
382            assert isinstance(result, Timestamp)
383            assert result == expected_localize
384
385            # see gh-14101
386            exp_warning = None
387            ts = Timestamp(dt, tz=tz) + Nano(5)
388
389            if (
390                type(offset_s).__name__ == "DateOffset"
391                and (funcname == "apply" or normalize)
392                and ts.nanosecond > 0
393            ):
394                exp_warning = UserWarning
395
396            # test nanosecond is preserved
397            with tm.assert_produces_warning(exp_warning, check_stacklevel=False):
398                result = func(ts)
399            assert isinstance(result, Timestamp)
400            if normalize is False:
401                assert result == expected_localize + Nano(5)
402            else:
403                assert result == expected_localize
404
405    def test_apply(self, offset_types):
406        sdt = datetime(2011, 1, 1, 9, 0)
407        ndt = np_datetime64_compat("2011-01-01 09:00Z")
408
409        for dt in [sdt, ndt]:
410            expected = self.expecteds[offset_types.__name__]
411            self._check_offsetfunc_works(offset_types, "apply", dt, expected)
412
413            expected = Timestamp(expected.date())
414            self._check_offsetfunc_works(
415                offset_types, "apply", dt, expected, normalize=True
416            )
417
418    def test_rollforward(self, offset_types):
419        expecteds = self.expecteds.copy()
420
421        # result will not be changed if the target is on the offset
422        no_changes = [
423            "Day",
424            "MonthBegin",
425            "SemiMonthBegin",
426            "YearBegin",
427            "Week",
428            "Hour",
429            "Minute",
430            "Second",
431            "Milli",
432            "Micro",
433            "Nano",
434            "DateOffset",
435        ]
436        for n in no_changes:
437            expecteds[n] = Timestamp("2011/01/01 09:00")
438
439        expecteds["BusinessHour"] = Timestamp("2011-01-03 09:00:00")
440        expecteds["CustomBusinessHour"] = Timestamp("2011-01-03 09:00:00")
441
442        # but be changed when normalize=True
443        norm_expected = expecteds.copy()
444        for k in norm_expected:
445            norm_expected[k] = Timestamp(norm_expected[k].date())
446
447        normalized = {
448            "Day": Timestamp("2011-01-02 00:00:00"),
449            "DateOffset": Timestamp("2011-01-02 00:00:00"),
450            "MonthBegin": Timestamp("2011-02-01 00:00:00"),
451            "SemiMonthBegin": Timestamp("2011-01-15 00:00:00"),
452            "YearBegin": Timestamp("2012-01-01 00:00:00"),
453            "Week": Timestamp("2011-01-08 00:00:00"),
454            "Hour": Timestamp("2011-01-01 00:00:00"),
455            "Minute": Timestamp("2011-01-01 00:00:00"),
456            "Second": Timestamp("2011-01-01 00:00:00"),
457            "Milli": Timestamp("2011-01-01 00:00:00"),
458            "Micro": Timestamp("2011-01-01 00:00:00"),
459        }
460        norm_expected.update(normalized)
461
462        sdt = datetime(2011, 1, 1, 9, 0)
463        ndt = np_datetime64_compat("2011-01-01 09:00Z")
464
465        for dt in [sdt, ndt]:
466            expected = expecteds[offset_types.__name__]
467            self._check_offsetfunc_works(offset_types, "rollforward", dt, expected)
468            expected = norm_expected[offset_types.__name__]
469            self._check_offsetfunc_works(
470                offset_types, "rollforward", dt, expected, normalize=True
471            )
472
473    def test_rollback(self, offset_types):
474        expecteds = {
475            "BusinessDay": Timestamp("2010-12-31 09:00:00"),
476            "CustomBusinessDay": Timestamp("2010-12-31 09:00:00"),
477            "CustomBusinessMonthEnd": Timestamp("2010-12-31 09:00:00"),
478            "CustomBusinessMonthBegin": Timestamp("2010-12-01 09:00:00"),
479            "BusinessMonthBegin": Timestamp("2010-12-01 09:00:00"),
480            "MonthEnd": Timestamp("2010-12-31 09:00:00"),
481            "SemiMonthEnd": Timestamp("2010-12-31 09:00:00"),
482            "BusinessMonthEnd": Timestamp("2010-12-31 09:00:00"),
483            "BYearBegin": Timestamp("2010-01-01 09:00:00"),
484            "YearEnd": Timestamp("2010-12-31 09:00:00"),
485            "BYearEnd": Timestamp("2010-12-31 09:00:00"),
486            "QuarterBegin": Timestamp("2010-12-01 09:00:00"),
487            "BQuarterBegin": Timestamp("2010-12-01 09:00:00"),
488            "QuarterEnd": Timestamp("2010-12-31 09:00:00"),
489            "BQuarterEnd": Timestamp("2010-12-31 09:00:00"),
490            "BusinessHour": Timestamp("2010-12-31 17:00:00"),
491            "CustomBusinessHour": Timestamp("2010-12-31 17:00:00"),
492            "WeekOfMonth": Timestamp("2010-12-11 09:00:00"),
493            "LastWeekOfMonth": Timestamp("2010-12-25 09:00:00"),
494            "FY5253Quarter": Timestamp("2010-10-26 09:00:00"),
495            "FY5253": Timestamp("2010-01-26 09:00:00"),
496            "Easter": Timestamp("2010-04-04 09:00:00"),
497        }
498
499        # result will not be changed if the target is on the offset
500        for n in [
501            "Day",
502            "MonthBegin",
503            "SemiMonthBegin",
504            "YearBegin",
505            "Week",
506            "Hour",
507            "Minute",
508            "Second",
509            "Milli",
510            "Micro",
511            "Nano",
512            "DateOffset",
513        ]:
514            expecteds[n] = Timestamp("2011/01/01 09:00")
515
516        # but be changed when normalize=True
517        norm_expected = expecteds.copy()
518        for k in norm_expected:
519            norm_expected[k] = Timestamp(norm_expected[k].date())
520
521        normalized = {
522            "Day": Timestamp("2010-12-31 00:00:00"),
523            "DateOffset": Timestamp("2010-12-31 00:00:00"),
524            "MonthBegin": Timestamp("2010-12-01 00:00:00"),
525            "SemiMonthBegin": Timestamp("2010-12-15 00:00:00"),
526            "YearBegin": Timestamp("2010-01-01 00:00:00"),
527            "Week": Timestamp("2010-12-25 00:00:00"),
528            "Hour": Timestamp("2011-01-01 00:00:00"),
529            "Minute": Timestamp("2011-01-01 00:00:00"),
530            "Second": Timestamp("2011-01-01 00:00:00"),
531            "Milli": Timestamp("2011-01-01 00:00:00"),
532            "Micro": Timestamp("2011-01-01 00:00:00"),
533        }
534        norm_expected.update(normalized)
535
536        sdt = datetime(2011, 1, 1, 9, 0)
537        ndt = np_datetime64_compat("2011-01-01 09:00Z")
538
539        for dt in [sdt, ndt]:
540            expected = expecteds[offset_types.__name__]
541            self._check_offsetfunc_works(offset_types, "rollback", dt, expected)
542
543            expected = norm_expected[offset_types.__name__]
544            self._check_offsetfunc_works(
545                offset_types, "rollback", dt, expected, normalize=True
546            )
547
548    def test_is_on_offset(self, offset_types):
549        dt = self.expecteds[offset_types.__name__]
550        offset_s = self._get_offset(offset_types)
551        assert offset_s.is_on_offset(dt)
552
553        # when normalize=True, is_on_offset checks time is 00:00:00
554        if issubclass(offset_types, Tick):
555            # normalize=True disallowed for Tick subclasses GH#21427
556            return
557        offset_n = self._get_offset(offset_types, normalize=True)
558        assert not offset_n.is_on_offset(dt)
559
560        if offset_types in (BusinessHour, CustomBusinessHour):
561            # In default BusinessHour (9:00-17:00), normalized time
562            # cannot be in business hour range
563            return
564        date = datetime(dt.year, dt.month, dt.day)
565        assert offset_n.is_on_offset(date)
566
567    def test_add(self, offset_types, tz_naive_fixture):
568        tz = tz_naive_fixture
569        dt = datetime(2011, 1, 1, 9, 0)
570
571        offset_s = self._get_offset(offset_types)
572        expected = self.expecteds[offset_types.__name__]
573
574        result_dt = dt + offset_s
575        result_ts = Timestamp(dt) + offset_s
576        for result in [result_dt, result_ts]:
577            assert isinstance(result, Timestamp)
578            assert result == expected
579
580        expected_localize = expected.tz_localize(tz)
581        result = Timestamp(dt, tz=tz) + offset_s
582        assert isinstance(result, Timestamp)
583        assert result == expected_localize
584
585        # normalize=True, disallowed for Tick subclasses GH#21427
586        if issubclass(offset_types, Tick):
587            return
588        offset_s = self._get_offset(offset_types, normalize=True)
589        expected = Timestamp(expected.date())
590
591        result_dt = dt + offset_s
592        result_ts = Timestamp(dt) + offset_s
593        for result in [result_dt, result_ts]:
594            assert isinstance(result, Timestamp)
595            assert result == expected
596
597        expected_localize = expected.tz_localize(tz)
598        result = Timestamp(dt, tz=tz) + offset_s
599        assert isinstance(result, Timestamp)
600        assert result == expected_localize
601
602    def test_add_empty_datetimeindex(self, offset_types, tz_naive_fixture):
603        # GH#12724, GH#30336
604        offset_s = self._get_offset(offset_types)
605
606        dti = DatetimeIndex([], tz=tz_naive_fixture)
607
608        warn = None
609        if isinstance(
610            offset_s,
611            (
612                Easter,
613                WeekOfMonth,
614                LastWeekOfMonth,
615                CustomBusinessDay,
616                BusinessHour,
617                CustomBusinessHour,
618                CustomBusinessMonthBegin,
619                CustomBusinessMonthEnd,
620                FY5253,
621                FY5253Quarter,
622            ),
623        ):
624            # We don't have an optimized apply_index
625            warn = PerformanceWarning
626
627        with tm.assert_produces_warning(warn):
628            result = dti + offset_s
629        tm.assert_index_equal(result, dti)
630        with tm.assert_produces_warning(warn):
631            result = offset_s + dti
632        tm.assert_index_equal(result, dti)
633
634        dta = dti._data
635        with tm.assert_produces_warning(warn):
636            result = dta + offset_s
637        tm.assert_equal(result, dta)
638        with tm.assert_produces_warning(warn):
639            result = offset_s + dta
640        tm.assert_equal(result, dta)
641
642    def test_pickle_roundtrip(self, offset_types):
643        off = self._get_offset(offset_types)
644        res = tm.round_trip_pickle(off)
645        assert off == res
646        if type(off) is not DateOffset:
647            for attr in off._attributes:
648                if attr == "calendar":
649                    # np.busdaycalendar __eq__ will return False;
650                    #  we check holidays and weekmask attrs so are OK
651                    continue
652                # Make sure nothings got lost from _params (which __eq__) is based on
653                assert getattr(off, attr) == getattr(res, attr)
654
655    def test_pickle_dateoffset_odd_inputs(self):
656        # GH#34511
657        off = DateOffset(months=12)
658        res = tm.round_trip_pickle(off)
659        assert off == res
660
661        base_dt = datetime(2020, 1, 1)
662        assert base_dt + off == base_dt + res
663
664    def test_onOffset_deprecated(self, offset_types):
665        # GH#30340 use idiomatic naming
666        off = self._get_offset(offset_types)
667
668        ts = Timestamp.now()
669        with tm.assert_produces_warning(FutureWarning):
670            result = off.onOffset(ts)
671
672        expected = off.is_on_offset(ts)
673        assert result == expected
674
675    def test_isAnchored_deprecated(self, offset_types):
676        # GH#30340 use idiomatic naming
677        off = self._get_offset(offset_types)
678
679        with tm.assert_produces_warning(FutureWarning):
680            result = off.isAnchored()
681
682        expected = off.is_anchored()
683        assert result == expected
684
685    def test_offsets_hashable(self, offset_types):
686        # GH: 37267
687        off = self._get_offset(offset_types)
688        assert hash(off) is not None
689
690
691class TestDateOffset(Base):
692    def setup_method(self, method):
693        self.d = Timestamp(datetime(2008, 1, 2))
694        _offset_map.clear()
695
696    def test_repr(self):
697        repr(DateOffset())
698        repr(DateOffset(2))
699        repr(2 * DateOffset())
700        repr(2 * DateOffset(months=2))
701
702    def test_mul(self):
703        assert DateOffset(2) == 2 * DateOffset(1)
704        assert DateOffset(2) == DateOffset(1) * 2
705
706    def test_constructor(self):
707
708        assert (self.d + DateOffset(months=2)) == datetime(2008, 3, 2)
709        assert (self.d - DateOffset(months=2)) == datetime(2007, 11, 2)
710
711        assert (self.d + DateOffset(2)) == datetime(2008, 1, 4)
712
713        assert not DateOffset(2).is_anchored()
714        assert DateOffset(1).is_anchored()
715
716        d = datetime(2008, 1, 31)
717        assert (d + DateOffset(months=1)) == datetime(2008, 2, 29)
718
719    def test_copy(self):
720        assert DateOffset(months=2).copy() == DateOffset(months=2)
721
722    def test_eq(self):
723        offset1 = DateOffset(days=1)
724        offset2 = DateOffset(days=365)
725
726        assert offset1 != offset2
727
728
729class TestBusinessDay(Base):
730    _offset = BDay
731
732    def setup_method(self, method):
733        self.d = datetime(2008, 1, 1)
734
735        self.offset = BDay()
736        self.offset1 = self.offset
737        self.offset2 = BDay(2)
738
739    def test_different_normalize_equals(self):
740        # GH#21404 changed __eq__ to return False when `normalize` does not match
741        offset = self._offset()
742        offset2 = self._offset(normalize=True)
743        assert offset != offset2
744
745    def test_repr(self):
746        assert repr(self.offset) == "<BusinessDay>"
747        assert repr(self.offset2) == "<2 * BusinessDays>"
748
749        expected = "<BusinessDay: offset=datetime.timedelta(days=1)>"
750        assert repr(self.offset + timedelta(1)) == expected
751
752    def test_with_offset(self):
753        offset = self.offset + timedelta(hours=2)
754
755        assert (self.d + offset) == datetime(2008, 1, 2, 2)
756
757    def test_with_offset_index(self):
758        dti = DatetimeIndex([self.d])
759        result = dti + (self.offset + timedelta(hours=2))
760
761        expected = DatetimeIndex([datetime(2008, 1, 2, 2)])
762        tm.assert_index_equal(result, expected)
763
764    def test_eq(self):
765        assert self.offset2 == self.offset2
766
767    def test_mul(self):
768        pass
769
770    def test_hash(self):
771        assert hash(self.offset2) == hash(self.offset2)
772
773    def test_call(self):
774        with tm.assert_produces_warning(FutureWarning):
775            # GH#34171 DateOffset.__call__ is deprecated
776            assert self.offset2(self.d) == datetime(2008, 1, 3)
777
778    def testRollback1(self):
779        assert BDay(10).rollback(self.d) == self.d
780
781    def testRollback2(self):
782        assert BDay(10).rollback(datetime(2008, 1, 5)) == datetime(2008, 1, 4)
783
784    def testRollforward1(self):
785        assert BDay(10).rollforward(self.d) == self.d
786
787    def testRollforward2(self):
788        assert BDay(10).rollforward(datetime(2008, 1, 5)) == datetime(2008, 1, 7)
789
790    def test_roll_date_object(self):
791        offset = BDay()
792
793        dt = date(2012, 9, 15)
794
795        result = offset.rollback(dt)
796        assert result == datetime(2012, 9, 14)
797
798        result = offset.rollforward(dt)
799        assert result == datetime(2012, 9, 17)
800
801        offset = offsets.Day()
802        result = offset.rollback(dt)
803        assert result == datetime(2012, 9, 15)
804
805        result = offset.rollforward(dt)
806        assert result == datetime(2012, 9, 15)
807
808    def test_is_on_offset(self):
809        tests = [
810            (BDay(), datetime(2008, 1, 1), True),
811            (BDay(), datetime(2008, 1, 5), False),
812        ]
813
814        for offset, d, expected in tests:
815            assert_is_on_offset(offset, d, expected)
816
817    apply_cases: _ApplyCases = []
818    apply_cases.append(
819        (
820            BDay(),
821            {
822                datetime(2008, 1, 1): datetime(2008, 1, 2),
823                datetime(2008, 1, 4): datetime(2008, 1, 7),
824                datetime(2008, 1, 5): datetime(2008, 1, 7),
825                datetime(2008, 1, 6): datetime(2008, 1, 7),
826                datetime(2008, 1, 7): datetime(2008, 1, 8),
827            },
828        )
829    )
830
831    apply_cases.append(
832        (
833            2 * BDay(),
834            {
835                datetime(2008, 1, 1): datetime(2008, 1, 3),
836                datetime(2008, 1, 4): datetime(2008, 1, 8),
837                datetime(2008, 1, 5): datetime(2008, 1, 8),
838                datetime(2008, 1, 6): datetime(2008, 1, 8),
839                datetime(2008, 1, 7): datetime(2008, 1, 9),
840            },
841        )
842    )
843
844    apply_cases.append(
845        (
846            -BDay(),
847            {
848                datetime(2008, 1, 1): datetime(2007, 12, 31),
849                datetime(2008, 1, 4): datetime(2008, 1, 3),
850                datetime(2008, 1, 5): datetime(2008, 1, 4),
851                datetime(2008, 1, 6): datetime(2008, 1, 4),
852                datetime(2008, 1, 7): datetime(2008, 1, 4),
853                datetime(2008, 1, 8): datetime(2008, 1, 7),
854            },
855        )
856    )
857
858    apply_cases.append(
859        (
860            -2 * BDay(),
861            {
862                datetime(2008, 1, 1): datetime(2007, 12, 28),
863                datetime(2008, 1, 4): datetime(2008, 1, 2),
864                datetime(2008, 1, 5): datetime(2008, 1, 3),
865                datetime(2008, 1, 6): datetime(2008, 1, 3),
866                datetime(2008, 1, 7): datetime(2008, 1, 3),
867                datetime(2008, 1, 8): datetime(2008, 1, 4),
868                datetime(2008, 1, 9): datetime(2008, 1, 7),
869            },
870        )
871    )
872
873    apply_cases.append(
874        (
875            BDay(0),
876            {
877                datetime(2008, 1, 1): datetime(2008, 1, 1),
878                datetime(2008, 1, 4): datetime(2008, 1, 4),
879                datetime(2008, 1, 5): datetime(2008, 1, 7),
880                datetime(2008, 1, 6): datetime(2008, 1, 7),
881                datetime(2008, 1, 7): datetime(2008, 1, 7),
882            },
883        )
884    )
885
886    @pytest.mark.parametrize("case", apply_cases)
887    def test_apply(self, case):
888        offset, cases = case
889        for base, expected in cases.items():
890            assert_offset_equal(offset, base, expected)
891
892    def test_apply_large_n(self):
893        dt = datetime(2012, 10, 23)
894
895        result = dt + BDay(10)
896        assert result == datetime(2012, 11, 6)
897
898        result = dt + BDay(100) - BDay(100)
899        assert result == dt
900
901        off = BDay() * 6
902        rs = datetime(2012, 1, 1) - off
903        xp = datetime(2011, 12, 23)
904        assert rs == xp
905
906        st = datetime(2011, 12, 18)
907        rs = st + off
908        xp = datetime(2011, 12, 26)
909        assert rs == xp
910
911        off = BDay() * 10
912        rs = datetime(2014, 1, 5) + off  # see #5890
913        xp = datetime(2014, 1, 17)
914        assert rs == xp
915
916    def test_apply_corner(self):
917        msg = "Only know how to combine business day with datetime or timedelta"
918        with pytest.raises(ApplyTypeError, match=msg):
919            BDay().apply(BMonthEnd())
920
921
922class TestBusinessHour(Base):
923    _offset = BusinessHour
924
925    def setup_method(self, method):
926        self.d = datetime(2014, 7, 1, 10, 00)
927
928        self.offset1 = BusinessHour()
929        self.offset2 = BusinessHour(n=3)
930
931        self.offset3 = BusinessHour(n=-1)
932        self.offset4 = BusinessHour(n=-4)
933
934        from datetime import time as dt_time
935
936        self.offset5 = BusinessHour(start=dt_time(11, 0), end=dt_time(14, 30))
937        self.offset6 = BusinessHour(start="20:00", end="05:00")
938        self.offset7 = BusinessHour(n=-2, start=dt_time(21, 30), end=dt_time(6, 30))
939        self.offset8 = BusinessHour(start=["09:00", "13:00"], end=["12:00", "17:00"])
940        self.offset9 = BusinessHour(
941            n=3, start=["09:00", "22:00"], end=["13:00", "03:00"]
942        )
943        self.offset10 = BusinessHour(
944            n=-1, start=["23:00", "13:00"], end=["02:00", "17:00"]
945        )
946
947    @pytest.mark.parametrize(
948        "start,end,match",
949        [
950            (
951                dt_time(11, 0, 5),
952                "17:00",
953                "time data must be specified only with hour and minute",
954            ),
955            ("AAA", "17:00", "time data must match '%H:%M' format"),
956            ("14:00:05", "17:00", "time data must match '%H:%M' format"),
957            ([], "17:00", "Must include at least 1 start time"),
958            ("09:00", [], "Must include at least 1 end time"),
959            (
960                ["09:00", "11:00"],
961                "17:00",
962                "number of starting time and ending time must be the same",
963            ),
964            (
965                ["09:00", "11:00"],
966                ["10:00"],
967                "number of starting time and ending time must be the same",
968            ),
969            (
970                ["09:00", "11:00"],
971                ["12:00", "20:00"],
972                r"invalid starting and ending time\(s\): opening hours should not "
973                "touch or overlap with one another",
974            ),
975            (
976                ["12:00", "20:00"],
977                ["09:00", "11:00"],
978                r"invalid starting and ending time\(s\): opening hours should not "
979                "touch or overlap with one another",
980            ),
981        ],
982    )
983    def test_constructor_errors(self, start, end, match):
984        with pytest.raises(ValueError, match=match):
985            BusinessHour(start=start, end=end)
986
987    def test_different_normalize_equals(self):
988        # GH#21404 changed __eq__ to return False when `normalize` does not match
989        offset = self._offset()
990        offset2 = self._offset(normalize=True)
991        assert offset != offset2
992
993    def test_repr(self):
994        assert repr(self.offset1) == "<BusinessHour: BH=09:00-17:00>"
995        assert repr(self.offset2) == "<3 * BusinessHours: BH=09:00-17:00>"
996        assert repr(self.offset3) == "<-1 * BusinessHour: BH=09:00-17:00>"
997        assert repr(self.offset4) == "<-4 * BusinessHours: BH=09:00-17:00>"
998
999        assert repr(self.offset5) == "<BusinessHour: BH=11:00-14:30>"
1000        assert repr(self.offset6) == "<BusinessHour: BH=20:00-05:00>"
1001        assert repr(self.offset7) == "<-2 * BusinessHours: BH=21:30-06:30>"
1002        assert repr(self.offset8) == "<BusinessHour: BH=09:00-12:00,13:00-17:00>"
1003        assert repr(self.offset9) == "<3 * BusinessHours: BH=09:00-13:00,22:00-03:00>"
1004        assert repr(self.offset10) == "<-1 * BusinessHour: BH=13:00-17:00,23:00-02:00>"
1005
1006    def test_with_offset(self):
1007        expected = Timestamp("2014-07-01 13:00")
1008
1009        assert self.d + BusinessHour() * 3 == expected
1010        assert self.d + BusinessHour(n=3) == expected
1011
1012    @pytest.mark.parametrize(
1013        "offset_name",
1014        ["offset1", "offset2", "offset3", "offset4", "offset8", "offset9", "offset10"],
1015    )
1016    def test_eq_attribute(self, offset_name):
1017        offset = getattr(self, offset_name)
1018        assert offset == offset
1019
1020    @pytest.mark.parametrize(
1021        "offset1,offset2",
1022        [
1023            (BusinessHour(start="09:00"), BusinessHour()),
1024            (
1025                BusinessHour(start=["23:00", "13:00"], end=["12:00", "17:00"]),
1026                BusinessHour(start=["13:00", "23:00"], end=["17:00", "12:00"]),
1027            ),
1028        ],
1029    )
1030    def test_eq(self, offset1, offset2):
1031        assert offset1 == offset2
1032
1033    @pytest.mark.parametrize(
1034        "offset1,offset2",
1035        [
1036            (BusinessHour(), BusinessHour(-1)),
1037            (BusinessHour(start="09:00"), BusinessHour(start="09:01")),
1038            (
1039                BusinessHour(start="09:00", end="17:00"),
1040                BusinessHour(start="17:00", end="09:01"),
1041            ),
1042            (
1043                BusinessHour(start=["13:00", "23:00"], end=["18:00", "07:00"]),
1044                BusinessHour(start=["13:00", "23:00"], end=["17:00", "12:00"]),
1045            ),
1046        ],
1047    )
1048    def test_neq(self, offset1, offset2):
1049        assert offset1 != offset2
1050
1051    @pytest.mark.parametrize(
1052        "offset_name",
1053        ["offset1", "offset2", "offset3", "offset4", "offset8", "offset9", "offset10"],
1054    )
1055    def test_hash(self, offset_name):
1056        offset = getattr(self, offset_name)
1057        assert offset == offset
1058
1059    def test_call(self):
1060        with tm.assert_produces_warning(FutureWarning):
1061            # GH#34171 DateOffset.__call__ is deprecated
1062            assert self.offset1(self.d) == datetime(2014, 7, 1, 11)
1063            assert self.offset2(self.d) == datetime(2014, 7, 1, 13)
1064            assert self.offset3(self.d) == datetime(2014, 6, 30, 17)
1065            assert self.offset4(self.d) == datetime(2014, 6, 30, 14)
1066            assert self.offset8(self.d) == datetime(2014, 7, 1, 11)
1067            assert self.offset9(self.d) == datetime(2014, 7, 1, 22)
1068            assert self.offset10(self.d) == datetime(2014, 7, 1, 1)
1069
1070    def test_sub(self):
1071        # we have to override test_sub here because self.offset2 is not
1072        # defined as self._offset(2)
1073        off = self.offset2
1074        msg = "Cannot subtract datetime from offset"
1075        with pytest.raises(TypeError, match=msg):
1076            off - self.d
1077        assert 2 * off - off == off
1078
1079        assert self.d - self.offset2 == self.d + self._offset(-3)
1080
1081    def testRollback1(self):
1082        assert self.offset1.rollback(self.d) == self.d
1083        assert self.offset2.rollback(self.d) == self.d
1084        assert self.offset3.rollback(self.d) == self.d
1085        assert self.offset4.rollback(self.d) == self.d
1086        assert self.offset5.rollback(self.d) == datetime(2014, 6, 30, 14, 30)
1087        assert self.offset6.rollback(self.d) == datetime(2014, 7, 1, 5, 0)
1088        assert self.offset7.rollback(self.d) == datetime(2014, 7, 1, 6, 30)
1089        assert self.offset8.rollback(self.d) == self.d
1090        assert self.offset9.rollback(self.d) == self.d
1091        assert self.offset10.rollback(self.d) == datetime(2014, 7, 1, 2)
1092
1093        d = datetime(2014, 7, 1, 0)
1094        assert self.offset1.rollback(d) == datetime(2014, 6, 30, 17)
1095        assert self.offset2.rollback(d) == datetime(2014, 6, 30, 17)
1096        assert self.offset3.rollback(d) == datetime(2014, 6, 30, 17)
1097        assert self.offset4.rollback(d) == datetime(2014, 6, 30, 17)
1098        assert self.offset5.rollback(d) == datetime(2014, 6, 30, 14, 30)
1099        assert self.offset6.rollback(d) == d
1100        assert self.offset7.rollback(d) == d
1101        assert self.offset8.rollback(d) == datetime(2014, 6, 30, 17)
1102        assert self.offset9.rollback(d) == d
1103        assert self.offset10.rollback(d) == d
1104
1105        assert self._offset(5).rollback(self.d) == self.d
1106
1107    def testRollback2(self):
1108        assert self._offset(-3).rollback(datetime(2014, 7, 5, 15, 0)) == datetime(
1109            2014, 7, 4, 17, 0
1110        )
1111
1112    def testRollforward1(self):
1113        assert self.offset1.rollforward(self.d) == self.d
1114        assert self.offset2.rollforward(self.d) == self.d
1115        assert self.offset3.rollforward(self.d) == self.d
1116        assert self.offset4.rollforward(self.d) == self.d
1117        assert self.offset5.rollforward(self.d) == datetime(2014, 7, 1, 11, 0)
1118        assert self.offset6.rollforward(self.d) == datetime(2014, 7, 1, 20, 0)
1119        assert self.offset7.rollforward(self.d) == datetime(2014, 7, 1, 21, 30)
1120        assert self.offset8.rollforward(self.d) == self.d
1121        assert self.offset9.rollforward(self.d) == self.d
1122        assert self.offset10.rollforward(self.d) == datetime(2014, 7, 1, 13)
1123
1124        d = datetime(2014, 7, 1, 0)
1125        assert self.offset1.rollforward(d) == datetime(2014, 7, 1, 9)
1126        assert self.offset2.rollforward(d) == datetime(2014, 7, 1, 9)
1127        assert self.offset3.rollforward(d) == datetime(2014, 7, 1, 9)
1128        assert self.offset4.rollforward(d) == datetime(2014, 7, 1, 9)
1129        assert self.offset5.rollforward(d) == datetime(2014, 7, 1, 11)
1130        assert self.offset6.rollforward(d) == d
1131        assert self.offset7.rollforward(d) == d
1132        assert self.offset8.rollforward(d) == datetime(2014, 7, 1, 9)
1133        assert self.offset9.rollforward(d) == d
1134        assert self.offset10.rollforward(d) == d
1135
1136        assert self._offset(5).rollforward(self.d) == self.d
1137
1138    def testRollforward2(self):
1139        assert self._offset(-3).rollforward(datetime(2014, 7, 5, 16, 0)) == datetime(
1140            2014, 7, 7, 9
1141        )
1142
1143    def test_roll_date_object(self):
1144        offset = BusinessHour()
1145
1146        dt = datetime(2014, 7, 6, 15, 0)
1147
1148        result = offset.rollback(dt)
1149        assert result == datetime(2014, 7, 4, 17)
1150
1151        result = offset.rollforward(dt)
1152        assert result == datetime(2014, 7, 7, 9)
1153
1154    normalize_cases = []
1155    normalize_cases.append(
1156        (
1157            BusinessHour(normalize=True),
1158            {
1159                datetime(2014, 7, 1, 8): datetime(2014, 7, 1),
1160                datetime(2014, 7, 1, 17): datetime(2014, 7, 2),
1161                datetime(2014, 7, 1, 16): datetime(2014, 7, 2),
1162                datetime(2014, 7, 1, 23): datetime(2014, 7, 2),
1163                datetime(2014, 7, 1, 0): datetime(2014, 7, 1),
1164                datetime(2014, 7, 4, 15): datetime(2014, 7, 4),
1165                datetime(2014, 7, 4, 15, 59): datetime(2014, 7, 4),
1166                datetime(2014, 7, 4, 16, 30): datetime(2014, 7, 7),
1167                datetime(2014, 7, 5, 23): datetime(2014, 7, 7),
1168                datetime(2014, 7, 6, 10): datetime(2014, 7, 7),
1169            },
1170        )
1171    )
1172
1173    normalize_cases.append(
1174        (
1175            BusinessHour(-1, normalize=True),
1176            {
1177                datetime(2014, 7, 1, 8): datetime(2014, 6, 30),
1178                datetime(2014, 7, 1, 17): datetime(2014, 7, 1),
1179                datetime(2014, 7, 1, 16): datetime(2014, 7, 1),
1180                datetime(2014, 7, 1, 10): datetime(2014, 6, 30),
1181                datetime(2014, 7, 1, 0): datetime(2014, 6, 30),
1182                datetime(2014, 7, 7, 10): datetime(2014, 7, 4),
1183                datetime(2014, 7, 7, 10, 1): datetime(2014, 7, 7),
1184                datetime(2014, 7, 5, 23): datetime(2014, 7, 4),
1185                datetime(2014, 7, 6, 10): datetime(2014, 7, 4),
1186            },
1187        )
1188    )
1189
1190    normalize_cases.append(
1191        (
1192            BusinessHour(1, normalize=True, start="17:00", end="04:00"),
1193            {
1194                datetime(2014, 7, 1, 8): datetime(2014, 7, 1),
1195                datetime(2014, 7, 1, 17): datetime(2014, 7, 1),
1196                datetime(2014, 7, 1, 23): datetime(2014, 7, 2),
1197                datetime(2014, 7, 2, 2): datetime(2014, 7, 2),
1198                datetime(2014, 7, 2, 3): datetime(2014, 7, 2),
1199                datetime(2014, 7, 4, 23): datetime(2014, 7, 5),
1200                datetime(2014, 7, 5, 2): datetime(2014, 7, 5),
1201                datetime(2014, 7, 7, 2): datetime(2014, 7, 7),
1202                datetime(2014, 7, 7, 17): datetime(2014, 7, 7),
1203            },
1204        )
1205    )
1206
1207    @pytest.mark.parametrize("case", normalize_cases)
1208    def test_normalize(self, case):
1209        offset, cases = case
1210        for dt, expected in cases.items():
1211            assert offset.apply(dt) == expected
1212
1213    on_offset_cases = []
1214    on_offset_cases.append(
1215        (
1216            BusinessHour(),
1217            {
1218                datetime(2014, 7, 1, 9): True,
1219                datetime(2014, 7, 1, 8, 59): False,
1220                datetime(2014, 7, 1, 8): False,
1221                datetime(2014, 7, 1, 17): True,
1222                datetime(2014, 7, 1, 17, 1): False,
1223                datetime(2014, 7, 1, 18): False,
1224                datetime(2014, 7, 5, 9): False,
1225                datetime(2014, 7, 6, 12): False,
1226            },
1227        )
1228    )
1229
1230    on_offset_cases.append(
1231        (
1232            BusinessHour(start="10:00", end="15:00"),
1233            {
1234                datetime(2014, 7, 1, 9): False,
1235                datetime(2014, 7, 1, 10): True,
1236                datetime(2014, 7, 1, 15): True,
1237                datetime(2014, 7, 1, 15, 1): False,
1238                datetime(2014, 7, 5, 12): False,
1239                datetime(2014, 7, 6, 12): False,
1240            },
1241        )
1242    )
1243
1244    on_offset_cases.append(
1245        (
1246            BusinessHour(start="19:00", end="05:00"),
1247            {
1248                datetime(2014, 7, 1, 9, 0): False,
1249                datetime(2014, 7, 1, 10, 0): False,
1250                datetime(2014, 7, 1, 15): False,
1251                datetime(2014, 7, 1, 15, 1): False,
1252                datetime(2014, 7, 5, 12, 0): False,
1253                datetime(2014, 7, 6, 12, 0): False,
1254                datetime(2014, 7, 1, 19, 0): True,
1255                datetime(2014, 7, 2, 0, 0): True,
1256                datetime(2014, 7, 4, 23): True,
1257                datetime(2014, 7, 5, 1): True,
1258                datetime(2014, 7, 5, 5, 0): True,
1259                datetime(2014, 7, 6, 23, 0): False,
1260                datetime(2014, 7, 7, 3, 0): False,
1261            },
1262        )
1263    )
1264
1265    on_offset_cases.append(
1266        (
1267            BusinessHour(start=["09:00", "13:00"], end=["12:00", "17:00"]),
1268            {
1269                datetime(2014, 7, 1, 9): True,
1270                datetime(2014, 7, 1, 8, 59): False,
1271                datetime(2014, 7, 1, 8): False,
1272                datetime(2014, 7, 1, 17): True,
1273                datetime(2014, 7, 1, 17, 1): False,
1274                datetime(2014, 7, 1, 18): False,
1275                datetime(2014, 7, 5, 9): False,
1276                datetime(2014, 7, 6, 12): False,
1277                datetime(2014, 7, 1, 12, 30): False,
1278            },
1279        )
1280    )
1281
1282    on_offset_cases.append(
1283        (
1284            BusinessHour(start=["19:00", "23:00"], end=["21:00", "05:00"]),
1285            {
1286                datetime(2014, 7, 1, 9, 0): False,
1287                datetime(2014, 7, 1, 10, 0): False,
1288                datetime(2014, 7, 1, 15): False,
1289                datetime(2014, 7, 1, 15, 1): False,
1290                datetime(2014, 7, 5, 12, 0): False,
1291                datetime(2014, 7, 6, 12, 0): False,
1292                datetime(2014, 7, 1, 19, 0): True,
1293                datetime(2014, 7, 2, 0, 0): True,
1294                datetime(2014, 7, 4, 23): True,
1295                datetime(2014, 7, 5, 1): True,
1296                datetime(2014, 7, 5, 5, 0): True,
1297                datetime(2014, 7, 6, 23, 0): False,
1298                datetime(2014, 7, 7, 3, 0): False,
1299                datetime(2014, 7, 4, 22): False,
1300            },
1301        )
1302    )
1303
1304    @pytest.mark.parametrize("case", on_offset_cases)
1305    def test_is_on_offset(self, case):
1306        offset, cases = case
1307        for dt, expected in cases.items():
1308            assert offset.is_on_offset(dt) == expected
1309
1310    opening_time_cases = []
1311    # opening time should be affected by sign of n, not by n's value and
1312    # end
1313    opening_time_cases.append(
1314        (
1315            [
1316                BusinessHour(),
1317                BusinessHour(n=2),
1318                BusinessHour(n=4),
1319                BusinessHour(end="10:00"),
1320                BusinessHour(n=2, end="4:00"),
1321                BusinessHour(n=4, end="15:00"),
1322            ],
1323            {
1324                datetime(2014, 7, 1, 11): (
1325                    datetime(2014, 7, 2, 9),
1326                    datetime(2014, 7, 1, 9),
1327                ),
1328                datetime(2014, 7, 1, 18): (
1329                    datetime(2014, 7, 2, 9),
1330                    datetime(2014, 7, 1, 9),
1331                ),
1332                datetime(2014, 7, 1, 23): (
1333                    datetime(2014, 7, 2, 9),
1334                    datetime(2014, 7, 1, 9),
1335                ),
1336                datetime(2014, 7, 2, 8): (
1337                    datetime(2014, 7, 2, 9),
1338                    datetime(2014, 7, 1, 9),
1339                ),
1340                # if timestamp is on opening time, next opening time is
1341                # as it is
1342                datetime(2014, 7, 2, 9): (
1343                    datetime(2014, 7, 2, 9),
1344                    datetime(2014, 7, 2, 9),
1345                ),
1346                datetime(2014, 7, 2, 10): (
1347                    datetime(2014, 7, 3, 9),
1348                    datetime(2014, 7, 2, 9),
1349                ),
1350                # 2014-07-05 is saturday
1351                datetime(2014, 7, 5, 10): (
1352                    datetime(2014, 7, 7, 9),
1353                    datetime(2014, 7, 4, 9),
1354                ),
1355                datetime(2014, 7, 4, 10): (
1356                    datetime(2014, 7, 7, 9),
1357                    datetime(2014, 7, 4, 9),
1358                ),
1359                datetime(2014, 7, 4, 23): (
1360                    datetime(2014, 7, 7, 9),
1361                    datetime(2014, 7, 4, 9),
1362                ),
1363                datetime(2014, 7, 6, 10): (
1364                    datetime(2014, 7, 7, 9),
1365                    datetime(2014, 7, 4, 9),
1366                ),
1367                datetime(2014, 7, 7, 5): (
1368                    datetime(2014, 7, 7, 9),
1369                    datetime(2014, 7, 4, 9),
1370                ),
1371                datetime(2014, 7, 7, 9, 1): (
1372                    datetime(2014, 7, 8, 9),
1373                    datetime(2014, 7, 7, 9),
1374                ),
1375            },
1376        )
1377    )
1378
1379    opening_time_cases.append(
1380        (
1381            [
1382                BusinessHour(start="11:15"),
1383                BusinessHour(n=2, start="11:15"),
1384                BusinessHour(n=3, start="11:15"),
1385                BusinessHour(start="11:15", end="10:00"),
1386                BusinessHour(n=2, start="11:15", end="4:00"),
1387                BusinessHour(n=3, start="11:15", end="15:00"),
1388            ],
1389            {
1390                datetime(2014, 7, 1, 11): (
1391                    datetime(2014, 7, 1, 11, 15),
1392                    datetime(2014, 6, 30, 11, 15),
1393                ),
1394                datetime(2014, 7, 1, 18): (
1395                    datetime(2014, 7, 2, 11, 15),
1396                    datetime(2014, 7, 1, 11, 15),
1397                ),
1398                datetime(2014, 7, 1, 23): (
1399                    datetime(2014, 7, 2, 11, 15),
1400                    datetime(2014, 7, 1, 11, 15),
1401                ),
1402                datetime(2014, 7, 2, 8): (
1403                    datetime(2014, 7, 2, 11, 15),
1404                    datetime(2014, 7, 1, 11, 15),
1405                ),
1406                datetime(2014, 7, 2, 9): (
1407                    datetime(2014, 7, 2, 11, 15),
1408                    datetime(2014, 7, 1, 11, 15),
1409                ),
1410                datetime(2014, 7, 2, 10): (
1411                    datetime(2014, 7, 2, 11, 15),
1412                    datetime(2014, 7, 1, 11, 15),
1413                ),
1414                datetime(2014, 7, 2, 11, 15): (
1415                    datetime(2014, 7, 2, 11, 15),
1416                    datetime(2014, 7, 2, 11, 15),
1417                ),
1418                datetime(2014, 7, 2, 11, 15, 1): (
1419                    datetime(2014, 7, 3, 11, 15),
1420                    datetime(2014, 7, 2, 11, 15),
1421                ),
1422                datetime(2014, 7, 5, 10): (
1423                    datetime(2014, 7, 7, 11, 15),
1424                    datetime(2014, 7, 4, 11, 15),
1425                ),
1426                datetime(2014, 7, 4, 10): (
1427                    datetime(2014, 7, 4, 11, 15),
1428                    datetime(2014, 7, 3, 11, 15),
1429                ),
1430                datetime(2014, 7, 4, 23): (
1431                    datetime(2014, 7, 7, 11, 15),
1432                    datetime(2014, 7, 4, 11, 15),
1433                ),
1434                datetime(2014, 7, 6, 10): (
1435                    datetime(2014, 7, 7, 11, 15),
1436                    datetime(2014, 7, 4, 11, 15),
1437                ),
1438                datetime(2014, 7, 7, 5): (
1439                    datetime(2014, 7, 7, 11, 15),
1440                    datetime(2014, 7, 4, 11, 15),
1441                ),
1442                datetime(2014, 7, 7, 9, 1): (
1443                    datetime(2014, 7, 7, 11, 15),
1444                    datetime(2014, 7, 4, 11, 15),
1445                ),
1446            },
1447        )
1448    )
1449
1450    opening_time_cases.append(
1451        (
1452            [
1453                BusinessHour(-1),
1454                BusinessHour(n=-2),
1455                BusinessHour(n=-4),
1456                BusinessHour(n=-1, end="10:00"),
1457                BusinessHour(n=-2, end="4:00"),
1458                BusinessHour(n=-4, end="15:00"),
1459            ],
1460            {
1461                datetime(2014, 7, 1, 11): (
1462                    datetime(2014, 7, 1, 9),
1463                    datetime(2014, 7, 2, 9),
1464                ),
1465                datetime(2014, 7, 1, 18): (
1466                    datetime(2014, 7, 1, 9),
1467                    datetime(2014, 7, 2, 9),
1468                ),
1469                datetime(2014, 7, 1, 23): (
1470                    datetime(2014, 7, 1, 9),
1471                    datetime(2014, 7, 2, 9),
1472                ),
1473                datetime(2014, 7, 2, 8): (
1474                    datetime(2014, 7, 1, 9),
1475                    datetime(2014, 7, 2, 9),
1476                ),
1477                datetime(2014, 7, 2, 9): (
1478                    datetime(2014, 7, 2, 9),
1479                    datetime(2014, 7, 2, 9),
1480                ),
1481                datetime(2014, 7, 2, 10): (
1482                    datetime(2014, 7, 2, 9),
1483                    datetime(2014, 7, 3, 9),
1484                ),
1485                datetime(2014, 7, 5, 10): (
1486                    datetime(2014, 7, 4, 9),
1487                    datetime(2014, 7, 7, 9),
1488                ),
1489                datetime(2014, 7, 4, 10): (
1490                    datetime(2014, 7, 4, 9),
1491                    datetime(2014, 7, 7, 9),
1492                ),
1493                datetime(2014, 7, 4, 23): (
1494                    datetime(2014, 7, 4, 9),
1495                    datetime(2014, 7, 7, 9),
1496                ),
1497                datetime(2014, 7, 6, 10): (
1498                    datetime(2014, 7, 4, 9),
1499                    datetime(2014, 7, 7, 9),
1500                ),
1501                datetime(2014, 7, 7, 5): (
1502                    datetime(2014, 7, 4, 9),
1503                    datetime(2014, 7, 7, 9),
1504                ),
1505                datetime(2014, 7, 7, 9): (
1506                    datetime(2014, 7, 7, 9),
1507                    datetime(2014, 7, 7, 9),
1508                ),
1509                datetime(2014, 7, 7, 9, 1): (
1510                    datetime(2014, 7, 7, 9),
1511                    datetime(2014, 7, 8, 9),
1512                ),
1513            },
1514        )
1515    )
1516
1517    opening_time_cases.append(
1518        (
1519            [
1520                BusinessHour(start="17:00", end="05:00"),
1521                BusinessHour(n=3, start="17:00", end="03:00"),
1522            ],
1523            {
1524                datetime(2014, 7, 1, 11): (
1525                    datetime(2014, 7, 1, 17),
1526                    datetime(2014, 6, 30, 17),
1527                ),
1528                datetime(2014, 7, 1, 18): (
1529                    datetime(2014, 7, 2, 17),
1530                    datetime(2014, 7, 1, 17),
1531                ),
1532                datetime(2014, 7, 1, 23): (
1533                    datetime(2014, 7, 2, 17),
1534                    datetime(2014, 7, 1, 17),
1535                ),
1536                datetime(2014, 7, 2, 8): (
1537                    datetime(2014, 7, 2, 17),
1538                    datetime(2014, 7, 1, 17),
1539                ),
1540                datetime(2014, 7, 2, 9): (
1541                    datetime(2014, 7, 2, 17),
1542                    datetime(2014, 7, 1, 17),
1543                ),
1544                datetime(2014, 7, 4, 17): (
1545                    datetime(2014, 7, 4, 17),
1546                    datetime(2014, 7, 4, 17),
1547                ),
1548                datetime(2014, 7, 5, 10): (
1549                    datetime(2014, 7, 7, 17),
1550                    datetime(2014, 7, 4, 17),
1551                ),
1552                datetime(2014, 7, 4, 10): (
1553                    datetime(2014, 7, 4, 17),
1554                    datetime(2014, 7, 3, 17),
1555                ),
1556                datetime(2014, 7, 4, 23): (
1557                    datetime(2014, 7, 7, 17),
1558                    datetime(2014, 7, 4, 17),
1559                ),
1560                datetime(2014, 7, 6, 10): (
1561                    datetime(2014, 7, 7, 17),
1562                    datetime(2014, 7, 4, 17),
1563                ),
1564                datetime(2014, 7, 7, 5): (
1565                    datetime(2014, 7, 7, 17),
1566                    datetime(2014, 7, 4, 17),
1567                ),
1568                datetime(2014, 7, 7, 17, 1): (
1569                    datetime(2014, 7, 8, 17),
1570                    datetime(2014, 7, 7, 17),
1571                ),
1572            },
1573        )
1574    )
1575
1576    opening_time_cases.append(
1577        (
1578            [
1579                BusinessHour(-1, start="17:00", end="05:00"),
1580                BusinessHour(n=-2, start="17:00", end="03:00"),
1581            ],
1582            {
1583                datetime(2014, 7, 1, 11): (
1584                    datetime(2014, 6, 30, 17),
1585                    datetime(2014, 7, 1, 17),
1586                ),
1587                datetime(2014, 7, 1, 18): (
1588                    datetime(2014, 7, 1, 17),
1589                    datetime(2014, 7, 2, 17),
1590                ),
1591                datetime(2014, 7, 1, 23): (
1592                    datetime(2014, 7, 1, 17),
1593                    datetime(2014, 7, 2, 17),
1594                ),
1595                datetime(2014, 7, 2, 8): (
1596                    datetime(2014, 7, 1, 17),
1597                    datetime(2014, 7, 2, 17),
1598                ),
1599                datetime(2014, 7, 2, 9): (
1600                    datetime(2014, 7, 1, 17),
1601                    datetime(2014, 7, 2, 17),
1602                ),
1603                datetime(2014, 7, 2, 16, 59): (
1604                    datetime(2014, 7, 1, 17),
1605                    datetime(2014, 7, 2, 17),
1606                ),
1607                datetime(2014, 7, 5, 10): (
1608                    datetime(2014, 7, 4, 17),
1609                    datetime(2014, 7, 7, 17),
1610                ),
1611                datetime(2014, 7, 4, 10): (
1612                    datetime(2014, 7, 3, 17),
1613                    datetime(2014, 7, 4, 17),
1614                ),
1615                datetime(2014, 7, 4, 23): (
1616                    datetime(2014, 7, 4, 17),
1617                    datetime(2014, 7, 7, 17),
1618                ),
1619                datetime(2014, 7, 6, 10): (
1620                    datetime(2014, 7, 4, 17),
1621                    datetime(2014, 7, 7, 17),
1622                ),
1623                datetime(2014, 7, 7, 5): (
1624                    datetime(2014, 7, 4, 17),
1625                    datetime(2014, 7, 7, 17),
1626                ),
1627                datetime(2014, 7, 7, 18): (
1628                    datetime(2014, 7, 7, 17),
1629                    datetime(2014, 7, 8, 17),
1630                ),
1631            },
1632        )
1633    )
1634
1635    opening_time_cases.append(
1636        (
1637            [
1638                BusinessHour(start=["11:15", "15:00"], end=["13:00", "20:00"]),
1639                BusinessHour(n=3, start=["11:15", "15:00"], end=["12:00", "20:00"]),
1640                BusinessHour(start=["11:15", "15:00"], end=["13:00", "17:00"]),
1641                BusinessHour(n=2, start=["11:15", "15:00"], end=["12:00", "03:00"]),
1642                BusinessHour(n=3, start=["11:15", "15:00"], end=["13:00", "16:00"]),
1643            ],
1644            {
1645                datetime(2014, 7, 1, 11): (
1646                    datetime(2014, 7, 1, 11, 15),
1647                    datetime(2014, 6, 30, 15),
1648                ),
1649                datetime(2014, 7, 1, 18): (
1650                    datetime(2014, 7, 2, 11, 15),
1651                    datetime(2014, 7, 1, 15),
1652                ),
1653                datetime(2014, 7, 1, 23): (
1654                    datetime(2014, 7, 2, 11, 15),
1655                    datetime(2014, 7, 1, 15),
1656                ),
1657                datetime(2014, 7, 2, 8): (
1658                    datetime(2014, 7, 2, 11, 15),
1659                    datetime(2014, 7, 1, 15),
1660                ),
1661                datetime(2014, 7, 2, 9): (
1662                    datetime(2014, 7, 2, 11, 15),
1663                    datetime(2014, 7, 1, 15),
1664                ),
1665                datetime(2014, 7, 2, 10): (
1666                    datetime(2014, 7, 2, 11, 15),
1667                    datetime(2014, 7, 1, 15),
1668                ),
1669                datetime(2014, 7, 2, 11, 15): (
1670                    datetime(2014, 7, 2, 11, 15),
1671                    datetime(2014, 7, 2, 11, 15),
1672                ),
1673                datetime(2014, 7, 2, 11, 15, 1): (
1674                    datetime(2014, 7, 2, 15),
1675                    datetime(2014, 7, 2, 11, 15),
1676                ),
1677                datetime(2014, 7, 5, 10): (
1678                    datetime(2014, 7, 7, 11, 15),
1679                    datetime(2014, 7, 4, 15),
1680                ),
1681                datetime(2014, 7, 4, 10): (
1682                    datetime(2014, 7, 4, 11, 15),
1683                    datetime(2014, 7, 3, 15),
1684                ),
1685                datetime(2014, 7, 4, 23): (
1686                    datetime(2014, 7, 7, 11, 15),
1687                    datetime(2014, 7, 4, 15),
1688                ),
1689                datetime(2014, 7, 6, 10): (
1690                    datetime(2014, 7, 7, 11, 15),
1691                    datetime(2014, 7, 4, 15),
1692                ),
1693                datetime(2014, 7, 7, 5): (
1694                    datetime(2014, 7, 7, 11, 15),
1695                    datetime(2014, 7, 4, 15),
1696                ),
1697                datetime(2014, 7, 7, 9, 1): (
1698                    datetime(2014, 7, 7, 11, 15),
1699                    datetime(2014, 7, 4, 15),
1700                ),
1701                datetime(2014, 7, 7, 12): (
1702                    datetime(2014, 7, 7, 15),
1703                    datetime(2014, 7, 7, 11, 15),
1704                ),
1705            },
1706        )
1707    )
1708
1709    opening_time_cases.append(
1710        (
1711            [
1712                BusinessHour(n=-1, start=["17:00", "08:00"], end=["05:00", "10:00"]),
1713                BusinessHour(n=-2, start=["08:00", "17:00"], end=["10:00", "03:00"]),
1714            ],
1715            {
1716                datetime(2014, 7, 1, 11): (
1717                    datetime(2014, 7, 1, 8),
1718                    datetime(2014, 7, 1, 17),
1719                ),
1720                datetime(2014, 7, 1, 18): (
1721                    datetime(2014, 7, 1, 17),
1722                    datetime(2014, 7, 2, 8),
1723                ),
1724                datetime(2014, 7, 1, 23): (
1725                    datetime(2014, 7, 1, 17),
1726                    datetime(2014, 7, 2, 8),
1727                ),
1728                datetime(2014, 7, 2, 8): (
1729                    datetime(2014, 7, 2, 8),
1730                    datetime(2014, 7, 2, 8),
1731                ),
1732                datetime(2014, 7, 2, 9): (
1733                    datetime(2014, 7, 2, 8),
1734                    datetime(2014, 7, 2, 17),
1735                ),
1736                datetime(2014, 7, 2, 16, 59): (
1737                    datetime(2014, 7, 2, 8),
1738                    datetime(2014, 7, 2, 17),
1739                ),
1740                datetime(2014, 7, 5, 10): (
1741                    datetime(2014, 7, 4, 17),
1742                    datetime(2014, 7, 7, 8),
1743                ),
1744                datetime(2014, 7, 4, 10): (
1745                    datetime(2014, 7, 4, 8),
1746                    datetime(2014, 7, 4, 17),
1747                ),
1748                datetime(2014, 7, 4, 23): (
1749                    datetime(2014, 7, 4, 17),
1750                    datetime(2014, 7, 7, 8),
1751                ),
1752                datetime(2014, 7, 6, 10): (
1753                    datetime(2014, 7, 4, 17),
1754                    datetime(2014, 7, 7, 8),
1755                ),
1756                datetime(2014, 7, 7, 5): (
1757                    datetime(2014, 7, 4, 17),
1758                    datetime(2014, 7, 7, 8),
1759                ),
1760                datetime(2014, 7, 7, 18): (
1761                    datetime(2014, 7, 7, 17),
1762                    datetime(2014, 7, 8, 8),
1763                ),
1764            },
1765        )
1766    )
1767
1768    @pytest.mark.parametrize("case", opening_time_cases)
1769    def test_opening_time(self, case):
1770        _offsets, cases = case
1771        for offset in _offsets:
1772            for dt, (exp_next, exp_prev) in cases.items():
1773                assert offset._next_opening_time(dt) == exp_next
1774                assert offset._prev_opening_time(dt) == exp_prev
1775
1776    apply_cases = []
1777    apply_cases.append(
1778        (
1779            BusinessHour(),
1780            {
1781                datetime(2014, 7, 1, 11): datetime(2014, 7, 1, 12),
1782                datetime(2014, 7, 1, 13): datetime(2014, 7, 1, 14),
1783                datetime(2014, 7, 1, 15): datetime(2014, 7, 1, 16),
1784                datetime(2014, 7, 1, 19): datetime(2014, 7, 2, 10),
1785                datetime(2014, 7, 1, 16): datetime(2014, 7, 2, 9),
1786                datetime(2014, 7, 1, 16, 30, 15): datetime(2014, 7, 2, 9, 30, 15),
1787                datetime(2014, 7, 1, 17): datetime(2014, 7, 2, 10),
1788                datetime(2014, 7, 2, 11): datetime(2014, 7, 2, 12),
1789                # out of business hours
1790                datetime(2014, 7, 2, 8): datetime(2014, 7, 2, 10),
1791                datetime(2014, 7, 2, 19): datetime(2014, 7, 3, 10),
1792                datetime(2014, 7, 2, 23): datetime(2014, 7, 3, 10),
1793                datetime(2014, 7, 3, 0): datetime(2014, 7, 3, 10),
1794                # saturday
1795                datetime(2014, 7, 5, 15): datetime(2014, 7, 7, 10),
1796                datetime(2014, 7, 4, 17): datetime(2014, 7, 7, 10),
1797                datetime(2014, 7, 4, 16, 30): datetime(2014, 7, 7, 9, 30),
1798                datetime(2014, 7, 4, 16, 30, 30): datetime(2014, 7, 7, 9, 30, 30),
1799            },
1800        )
1801    )
1802
1803    apply_cases.append(
1804        (
1805            BusinessHour(4),
1806            {
1807                datetime(2014, 7, 1, 11): datetime(2014, 7, 1, 15),
1808                datetime(2014, 7, 1, 13): datetime(2014, 7, 2, 9),
1809                datetime(2014, 7, 1, 15): datetime(2014, 7, 2, 11),
1810                datetime(2014, 7, 1, 16): datetime(2014, 7, 2, 12),
1811                datetime(2014, 7, 1, 17): datetime(2014, 7, 2, 13),
1812                datetime(2014, 7, 2, 11): datetime(2014, 7, 2, 15),
1813                datetime(2014, 7, 2, 8): datetime(2014, 7, 2, 13),
1814                datetime(2014, 7, 2, 19): datetime(2014, 7, 3, 13),
1815                datetime(2014, 7, 2, 23): datetime(2014, 7, 3, 13),
1816                datetime(2014, 7, 3, 0): datetime(2014, 7, 3, 13),
1817                datetime(2014, 7, 5, 15): datetime(2014, 7, 7, 13),
1818                datetime(2014, 7, 4, 17): datetime(2014, 7, 7, 13),
1819                datetime(2014, 7, 4, 16, 30): datetime(2014, 7, 7, 12, 30),
1820                datetime(2014, 7, 4, 16, 30, 30): datetime(2014, 7, 7, 12, 30, 30),
1821            },
1822        )
1823    )
1824
1825    apply_cases.append(
1826        (
1827            BusinessHour(-1),
1828            {
1829                datetime(2014, 7, 1, 11): datetime(2014, 7, 1, 10),
1830                datetime(2014, 7, 1, 13): datetime(2014, 7, 1, 12),
1831                datetime(2014, 7, 1, 15): datetime(2014, 7, 1, 14),
1832                datetime(2014, 7, 1, 16): datetime(2014, 7, 1, 15),
1833                datetime(2014, 7, 1, 10): datetime(2014, 6, 30, 17),
1834                datetime(2014, 7, 1, 16, 30, 15): datetime(2014, 7, 1, 15, 30, 15),
1835                datetime(2014, 7, 1, 9, 30, 15): datetime(2014, 6, 30, 16, 30, 15),
1836                datetime(2014, 7, 1, 17): datetime(2014, 7, 1, 16),
1837                datetime(2014, 7, 1, 5): datetime(2014, 6, 30, 16),
1838                datetime(2014, 7, 2, 11): datetime(2014, 7, 2, 10),
1839                # out of business hours
1840                datetime(2014, 7, 2, 8): datetime(2014, 7, 1, 16),
1841                datetime(2014, 7, 2, 19): datetime(2014, 7, 2, 16),
1842                datetime(2014, 7, 2, 23): datetime(2014, 7, 2, 16),
1843                datetime(2014, 7, 3, 0): datetime(2014, 7, 2, 16),
1844                # saturday
1845                datetime(2014, 7, 5, 15): datetime(2014, 7, 4, 16),
1846                datetime(2014, 7, 7, 9): datetime(2014, 7, 4, 16),
1847                datetime(2014, 7, 7, 9, 30): datetime(2014, 7, 4, 16, 30),
1848                datetime(2014, 7, 7, 9, 30, 30): datetime(2014, 7, 4, 16, 30, 30),
1849            },
1850        )
1851    )
1852
1853    apply_cases.append(
1854        (
1855            BusinessHour(-4),
1856            {
1857                datetime(2014, 7, 1, 11): datetime(2014, 6, 30, 15),
1858                datetime(2014, 7, 1, 13): datetime(2014, 6, 30, 17),
1859                datetime(2014, 7, 1, 15): datetime(2014, 7, 1, 11),
1860                datetime(2014, 7, 1, 16): datetime(2014, 7, 1, 12),
1861                datetime(2014, 7, 1, 17): datetime(2014, 7, 1, 13),
1862                datetime(2014, 7, 2, 11): datetime(2014, 7, 1, 15),
1863                datetime(2014, 7, 2, 8): datetime(2014, 7, 1, 13),
1864                datetime(2014, 7, 2, 19): datetime(2014, 7, 2, 13),
1865                datetime(2014, 7, 2, 23): datetime(2014, 7, 2, 13),
1866                datetime(2014, 7, 3, 0): datetime(2014, 7, 2, 13),
1867                datetime(2014, 7, 5, 15): datetime(2014, 7, 4, 13),
1868                datetime(2014, 7, 4, 18): datetime(2014, 7, 4, 13),
1869                datetime(2014, 7, 7, 9, 30): datetime(2014, 7, 4, 13, 30),
1870                datetime(2014, 7, 7, 9, 30, 30): datetime(2014, 7, 4, 13, 30, 30),
1871            },
1872        )
1873    )
1874
1875    apply_cases.append(
1876        (
1877            BusinessHour(start="13:00", end="16:00"),
1878            {
1879                datetime(2014, 7, 1, 11): datetime(2014, 7, 1, 14),
1880                datetime(2014, 7, 1, 13): datetime(2014, 7, 1, 14),
1881                datetime(2014, 7, 1, 15): datetime(2014, 7, 2, 13),
1882                datetime(2014, 7, 1, 19): datetime(2014, 7, 2, 14),
1883                datetime(2014, 7, 1, 16): datetime(2014, 7, 2, 14),
1884                datetime(2014, 7, 1, 15, 30, 15): datetime(2014, 7, 2, 13, 30, 15),
1885                datetime(2014, 7, 5, 15): datetime(2014, 7, 7, 14),
1886                datetime(2014, 7, 4, 17): datetime(2014, 7, 7, 14),
1887            },
1888        )
1889    )
1890
1891    apply_cases.append(
1892        (
1893            BusinessHour(n=2, start="13:00", end="16:00"),
1894            {
1895                datetime(2014, 7, 1, 17): datetime(2014, 7, 2, 15),
1896                datetime(2014, 7, 2, 14): datetime(2014, 7, 3, 13),
1897                datetime(2014, 7, 2, 8): datetime(2014, 7, 2, 15),
1898                datetime(2014, 7, 2, 19): datetime(2014, 7, 3, 15),
1899                datetime(2014, 7, 2, 14, 30): datetime(2014, 7, 3, 13, 30),
1900                datetime(2014, 7, 3, 0): datetime(2014, 7, 3, 15),
1901                datetime(2014, 7, 5, 15): datetime(2014, 7, 7, 15),
1902                datetime(2014, 7, 4, 17): datetime(2014, 7, 7, 15),
1903                datetime(2014, 7, 4, 14, 30): datetime(2014, 7, 7, 13, 30),
1904                datetime(2014, 7, 4, 14, 30, 30): datetime(2014, 7, 7, 13, 30, 30),
1905            },
1906        )
1907    )
1908
1909    apply_cases.append(
1910        (
1911            BusinessHour(n=-1, start="13:00", end="16:00"),
1912            {
1913                datetime(2014, 7, 2, 11): datetime(2014, 7, 1, 15),
1914                datetime(2014, 7, 2, 13): datetime(2014, 7, 1, 15),
1915                datetime(2014, 7, 2, 14): datetime(2014, 7, 1, 16),
1916                datetime(2014, 7, 2, 15): datetime(2014, 7, 2, 14),
1917                datetime(2014, 7, 2, 19): datetime(2014, 7, 2, 15),
1918                datetime(2014, 7, 2, 16): datetime(2014, 7, 2, 15),
1919                datetime(2014, 7, 2, 13, 30, 15): datetime(2014, 7, 1, 15, 30, 15),
1920                datetime(2014, 7, 5, 15): datetime(2014, 7, 4, 15),
1921                datetime(2014, 7, 7, 11): datetime(2014, 7, 4, 15),
1922            },
1923        )
1924    )
1925
1926    apply_cases.append(
1927        (
1928            BusinessHour(n=-3, start="10:00", end="16:00"),
1929            {
1930                datetime(2014, 7, 1, 17): datetime(2014, 7, 1, 13),
1931                datetime(2014, 7, 2, 14): datetime(2014, 7, 2, 11),
1932                datetime(2014, 7, 2, 8): datetime(2014, 7, 1, 13),
1933                datetime(2014, 7, 2, 13): datetime(2014, 7, 1, 16),
1934                datetime(2014, 7, 2, 19): datetime(2014, 7, 2, 13),
1935                datetime(2014, 7, 2, 11, 30): datetime(2014, 7, 1, 14, 30),
1936                datetime(2014, 7, 3, 0): datetime(2014, 7, 2, 13),
1937                datetime(2014, 7, 4, 10): datetime(2014, 7, 3, 13),
1938                datetime(2014, 7, 5, 15): datetime(2014, 7, 4, 13),
1939                datetime(2014, 7, 4, 16): datetime(2014, 7, 4, 13),
1940                datetime(2014, 7, 4, 12, 30): datetime(2014, 7, 3, 15, 30),
1941                datetime(2014, 7, 4, 12, 30, 30): datetime(2014, 7, 3, 15, 30, 30),
1942            },
1943        )
1944    )
1945
1946    apply_cases.append(
1947        (
1948            BusinessHour(start="19:00", end="05:00"),
1949            {
1950                datetime(2014, 7, 1, 17): datetime(2014, 7, 1, 20),
1951                datetime(2014, 7, 2, 14): datetime(2014, 7, 2, 20),
1952                datetime(2014, 7, 2, 8): datetime(2014, 7, 2, 20),
1953                datetime(2014, 7, 2, 13): datetime(2014, 7, 2, 20),
1954                datetime(2014, 7, 2, 19): datetime(2014, 7, 2, 20),
1955                datetime(2014, 7, 2, 4, 30): datetime(2014, 7, 2, 19, 30),
1956                datetime(2014, 7, 3, 0): datetime(2014, 7, 3, 1),
1957                datetime(2014, 7, 4, 10): datetime(2014, 7, 4, 20),
1958                datetime(2014, 7, 4, 23): datetime(2014, 7, 5, 0),
1959                datetime(2014, 7, 5, 0): datetime(2014, 7, 5, 1),
1960                datetime(2014, 7, 5, 4): datetime(2014, 7, 7, 19),
1961                datetime(2014, 7, 5, 4, 30): datetime(2014, 7, 7, 19, 30),
1962                datetime(2014, 7, 5, 4, 30, 30): datetime(2014, 7, 7, 19, 30, 30),
1963            },
1964        )
1965    )
1966
1967    apply_cases.append(
1968        (
1969            BusinessHour(n=-1, start="19:00", end="05:00"),
1970            {
1971                datetime(2014, 7, 1, 17): datetime(2014, 7, 1, 4),
1972                datetime(2014, 7, 2, 14): datetime(2014, 7, 2, 4),
1973                datetime(2014, 7, 2, 8): datetime(2014, 7, 2, 4),
1974                datetime(2014, 7, 2, 13): datetime(2014, 7, 2, 4),
1975                datetime(2014, 7, 2, 20): datetime(2014, 7, 2, 5),
1976                datetime(2014, 7, 2, 19): datetime(2014, 7, 2, 4),
1977                datetime(2014, 7, 2, 19, 30): datetime(2014, 7, 2, 4, 30),
1978                datetime(2014, 7, 3, 0): datetime(2014, 7, 2, 23),
1979                datetime(2014, 7, 3, 6): datetime(2014, 7, 3, 4),
1980                datetime(2014, 7, 4, 23): datetime(2014, 7, 4, 22),
1981                datetime(2014, 7, 5, 0): datetime(2014, 7, 4, 23),
1982                datetime(2014, 7, 5, 4): datetime(2014, 7, 5, 3),
1983                datetime(2014, 7, 7, 19, 30): datetime(2014, 7, 5, 4, 30),
1984                datetime(2014, 7, 7, 19, 30, 30): datetime(2014, 7, 5, 4, 30, 30),
1985            },
1986        )
1987    )
1988
1989    # long business hours (see gh-26381)
1990    apply_cases.append(
1991        (
1992            BusinessHour(n=4, start="00:00", end="23:00"),
1993            {
1994                datetime(2014, 7, 3, 22): datetime(2014, 7, 4, 3),
1995                datetime(2014, 7, 4, 22): datetime(2014, 7, 7, 3),
1996                datetime(2014, 7, 3, 22, 30): datetime(2014, 7, 4, 3, 30),
1997                datetime(2014, 7, 3, 22, 20): datetime(2014, 7, 4, 3, 20),
1998                datetime(2014, 7, 4, 22, 30, 30): datetime(2014, 7, 7, 3, 30, 30),
1999                datetime(2014, 7, 4, 22, 30, 20): datetime(2014, 7, 7, 3, 30, 20),
2000            },
2001        )
2002    )
2003
2004    apply_cases.append(
2005        (
2006            BusinessHour(n=-4, start="00:00", end="23:00"),
2007            {
2008                datetime(2014, 7, 4, 3): datetime(2014, 7, 3, 22),
2009                datetime(2014, 7, 7, 3): datetime(2014, 7, 4, 22),
2010                datetime(2014, 7, 4, 3, 30): datetime(2014, 7, 3, 22, 30),
2011                datetime(2014, 7, 4, 3, 20): datetime(2014, 7, 3, 22, 20),
2012                datetime(2014, 7, 7, 3, 30, 30): datetime(2014, 7, 4, 22, 30, 30),
2013                datetime(2014, 7, 7, 3, 30, 20): datetime(2014, 7, 4, 22, 30, 20),
2014            },
2015        )
2016    )
2017
2018    # multiple business hours
2019    apply_cases.append(
2020        (
2021            BusinessHour(start=["09:00", "14:00"], end=["12:00", "18:00"]),
2022            {
2023                datetime(2014, 7, 1, 11): datetime(2014, 7, 1, 14),
2024                datetime(2014, 7, 1, 15): datetime(2014, 7, 1, 16),
2025                datetime(2014, 7, 1, 19): datetime(2014, 7, 2, 10),
2026                datetime(2014, 7, 1, 16): datetime(2014, 7, 1, 17),
2027                datetime(2014, 7, 1, 16, 30, 15): datetime(2014, 7, 1, 17, 30, 15),
2028                datetime(2014, 7, 1, 17): datetime(2014, 7, 2, 9),
2029                datetime(2014, 7, 2, 11): datetime(2014, 7, 2, 14),
2030                # out of business hours
2031                datetime(2014, 7, 1, 13): datetime(2014, 7, 1, 15),
2032                datetime(2014, 7, 2, 8): datetime(2014, 7, 2, 10),
2033                datetime(2014, 7, 2, 19): datetime(2014, 7, 3, 10),
2034                datetime(2014, 7, 2, 23): datetime(2014, 7, 3, 10),
2035                datetime(2014, 7, 3, 0): datetime(2014, 7, 3, 10),
2036                # saturday
2037                datetime(2014, 7, 5, 15): datetime(2014, 7, 7, 10),
2038                datetime(2014, 7, 4, 17): datetime(2014, 7, 7, 9),
2039                datetime(2014, 7, 4, 17, 30): datetime(2014, 7, 7, 9, 30),
2040                datetime(2014, 7, 4, 17, 30, 30): datetime(2014, 7, 7, 9, 30, 30),
2041            },
2042        )
2043    )
2044
2045    apply_cases.append(
2046        (
2047            BusinessHour(n=4, start=["09:00", "14:00"], end=["12:00", "18:00"]),
2048            {
2049                datetime(2014, 7, 1, 11): datetime(2014, 7, 1, 17),
2050                datetime(2014, 7, 1, 13): datetime(2014, 7, 2, 9),
2051                datetime(2014, 7, 1, 15): datetime(2014, 7, 2, 10),
2052                datetime(2014, 7, 1, 16): datetime(2014, 7, 2, 11),
2053                datetime(2014, 7, 1, 17): datetime(2014, 7, 2, 14),
2054                datetime(2014, 7, 2, 11): datetime(2014, 7, 2, 17),
2055                datetime(2014, 7, 2, 8): datetime(2014, 7, 2, 15),
2056                datetime(2014, 7, 2, 19): datetime(2014, 7, 3, 15),
2057                datetime(2014, 7, 2, 23): datetime(2014, 7, 3, 15),
2058                datetime(2014, 7, 3, 0): datetime(2014, 7, 3, 15),
2059                datetime(2014, 7, 5, 15): datetime(2014, 7, 7, 15),
2060                datetime(2014, 7, 4, 17): datetime(2014, 7, 7, 14),
2061                datetime(2014, 7, 4, 16, 30): datetime(2014, 7, 7, 11, 30),
2062                datetime(2014, 7, 4, 16, 30, 30): datetime(2014, 7, 7, 11, 30, 30),
2063            },
2064        )
2065    )
2066
2067    apply_cases.append(
2068        (
2069            BusinessHour(n=-4, start=["09:00", "14:00"], end=["12:00", "18:00"]),
2070            {
2071                datetime(2014, 7, 1, 11): datetime(2014, 6, 30, 16),
2072                datetime(2014, 7, 1, 13): datetime(2014, 6, 30, 17),
2073                datetime(2014, 7, 1, 15): datetime(2014, 6, 30, 18),
2074                datetime(2014, 7, 1, 16): datetime(2014, 7, 1, 10),
2075                datetime(2014, 7, 1, 17): datetime(2014, 7, 1, 11),
2076                datetime(2014, 7, 2, 11): datetime(2014, 7, 1, 16),
2077                datetime(2014, 7, 2, 8): datetime(2014, 7, 1, 12),
2078                datetime(2014, 7, 2, 19): datetime(2014, 7, 2, 12),
2079                datetime(2014, 7, 2, 23): datetime(2014, 7, 2, 12),
2080                datetime(2014, 7, 3, 0): datetime(2014, 7, 2, 12),
2081                datetime(2014, 7, 5, 15): datetime(2014, 7, 4, 12),
2082                datetime(2014, 7, 4, 18): datetime(2014, 7, 4, 12),
2083                datetime(2014, 7, 7, 9, 30): datetime(2014, 7, 4, 14, 30),
2084                datetime(2014, 7, 7, 9, 30, 30): datetime(2014, 7, 4, 14, 30, 30),
2085            },
2086        )
2087    )
2088
2089    apply_cases.append(
2090        (
2091            BusinessHour(n=-1, start=["19:00", "03:00"], end=["01:00", "05:00"]),
2092            {
2093                datetime(2014, 7, 1, 17): datetime(2014, 7, 1, 4),
2094                datetime(2014, 7, 2, 14): datetime(2014, 7, 2, 4),
2095                datetime(2014, 7, 2, 8): datetime(2014, 7, 2, 4),
2096                datetime(2014, 7, 2, 13): datetime(2014, 7, 2, 4),
2097                datetime(2014, 7, 2, 20): datetime(2014, 7, 2, 5),
2098                datetime(2014, 7, 2, 19): datetime(2014, 7, 2, 4),
2099                datetime(2014, 7, 2, 4): datetime(2014, 7, 2, 1),
2100                datetime(2014, 7, 2, 19, 30): datetime(2014, 7, 2, 4, 30),
2101                datetime(2014, 7, 3, 0): datetime(2014, 7, 2, 23),
2102                datetime(2014, 7, 3, 6): datetime(2014, 7, 3, 4),
2103                datetime(2014, 7, 4, 23): datetime(2014, 7, 4, 22),
2104                datetime(2014, 7, 5, 0): datetime(2014, 7, 4, 23),
2105                datetime(2014, 7, 5, 4): datetime(2014, 7, 5, 0),
2106                datetime(2014, 7, 7, 3, 30): datetime(2014, 7, 5, 0, 30),
2107                datetime(2014, 7, 7, 19, 30): datetime(2014, 7, 7, 4, 30),
2108                datetime(2014, 7, 7, 19, 30, 30): datetime(2014, 7, 7, 4, 30, 30),
2109            },
2110        )
2111    )
2112
2113    @pytest.mark.parametrize("case", apply_cases)
2114    def test_apply(self, case):
2115        offset, cases = case
2116        for base, expected in cases.items():
2117            assert_offset_equal(offset, base, expected)
2118
2119    apply_large_n_cases = []
2120    # A week later
2121    apply_large_n_cases.append(
2122        (
2123            BusinessHour(40),
2124            {
2125                datetime(2014, 7, 1, 11): datetime(2014, 7, 8, 11),
2126                datetime(2014, 7, 1, 13): datetime(2014, 7, 8, 13),
2127                datetime(2014, 7, 1, 15): datetime(2014, 7, 8, 15),
2128                datetime(2014, 7, 1, 16): datetime(2014, 7, 8, 16),
2129                datetime(2014, 7, 1, 17): datetime(2014, 7, 9, 9),
2130                datetime(2014, 7, 2, 11): datetime(2014, 7, 9, 11),
2131                datetime(2014, 7, 2, 8): datetime(2014, 7, 9, 9),
2132                datetime(2014, 7, 2, 19): datetime(2014, 7, 10, 9),
2133                datetime(2014, 7, 2, 23): datetime(2014, 7, 10, 9),
2134                datetime(2014, 7, 3, 0): datetime(2014, 7, 10, 9),
2135                datetime(2014, 7, 5, 15): datetime(2014, 7, 14, 9),
2136                datetime(2014, 7, 4, 18): datetime(2014, 7, 14, 9),
2137                datetime(2014, 7, 7, 9, 30): datetime(2014, 7, 14, 9, 30),
2138                datetime(2014, 7, 7, 9, 30, 30): datetime(2014, 7, 14, 9, 30, 30),
2139            },
2140        )
2141    )
2142
2143    # 3 days and 1 hour before
2144    apply_large_n_cases.append(
2145        (
2146            BusinessHour(-25),
2147            {
2148                datetime(2014, 7, 1, 11): datetime(2014, 6, 26, 10),
2149                datetime(2014, 7, 1, 13): datetime(2014, 6, 26, 12),
2150                datetime(2014, 7, 1, 9): datetime(2014, 6, 25, 16),
2151                datetime(2014, 7, 1, 10): datetime(2014, 6, 25, 17),
2152                datetime(2014, 7, 3, 11): datetime(2014, 6, 30, 10),
2153                datetime(2014, 7, 3, 8): datetime(2014, 6, 27, 16),
2154                datetime(2014, 7, 3, 19): datetime(2014, 6, 30, 16),
2155                datetime(2014, 7, 3, 23): datetime(2014, 6, 30, 16),
2156                datetime(2014, 7, 4, 9): datetime(2014, 6, 30, 16),
2157                datetime(2014, 7, 5, 15): datetime(2014, 7, 1, 16),
2158                datetime(2014, 7, 6, 18): datetime(2014, 7, 1, 16),
2159                datetime(2014, 7, 7, 9, 30): datetime(2014, 7, 1, 16, 30),
2160                datetime(2014, 7, 7, 10, 30, 30): datetime(2014, 7, 2, 9, 30, 30),
2161            },
2162        )
2163    )
2164
2165    # 5 days and 3 hours later
2166    apply_large_n_cases.append(
2167        (
2168            BusinessHour(28, start="21:00", end="02:00"),
2169            {
2170                datetime(2014, 7, 1, 11): datetime(2014, 7, 9, 0),
2171                datetime(2014, 7, 1, 22): datetime(2014, 7, 9, 1),
2172                datetime(2014, 7, 1, 23): datetime(2014, 7, 9, 21),
2173                datetime(2014, 7, 2, 2): datetime(2014, 7, 10, 0),
2174                datetime(2014, 7, 3, 21): datetime(2014, 7, 11, 0),
2175                datetime(2014, 7, 4, 1): datetime(2014, 7, 11, 23),
2176                datetime(2014, 7, 4, 2): datetime(2014, 7, 12, 0),
2177                datetime(2014, 7, 4, 3): datetime(2014, 7, 12, 0),
2178                datetime(2014, 7, 5, 1): datetime(2014, 7, 14, 23),
2179                datetime(2014, 7, 5, 15): datetime(2014, 7, 15, 0),
2180                datetime(2014, 7, 6, 18): datetime(2014, 7, 15, 0),
2181                datetime(2014, 7, 7, 1): datetime(2014, 7, 15, 0),
2182                datetime(2014, 7, 7, 23, 30): datetime(2014, 7, 15, 21, 30),
2183            },
2184        )
2185    )
2186
2187    # large n for multiple opening hours (3 days and 1 hour before)
2188    apply_large_n_cases.append(
2189        (
2190            BusinessHour(n=-25, start=["09:00", "14:00"], end=["12:00", "19:00"]),
2191            {
2192                datetime(2014, 7, 1, 11): datetime(2014, 6, 26, 10),
2193                datetime(2014, 7, 1, 13): datetime(2014, 6, 26, 11),
2194                datetime(2014, 7, 1, 9): datetime(2014, 6, 25, 18),
2195                datetime(2014, 7, 1, 10): datetime(2014, 6, 25, 19),
2196                datetime(2014, 7, 3, 11): datetime(2014, 6, 30, 10),
2197                datetime(2014, 7, 3, 8): datetime(2014, 6, 27, 18),
2198                datetime(2014, 7, 3, 19): datetime(2014, 6, 30, 18),
2199                datetime(2014, 7, 3, 23): datetime(2014, 6, 30, 18),
2200                datetime(2014, 7, 4, 9): datetime(2014, 6, 30, 18),
2201                datetime(2014, 7, 5, 15): datetime(2014, 7, 1, 18),
2202                datetime(2014, 7, 6, 18): datetime(2014, 7, 1, 18),
2203                datetime(2014, 7, 7, 9, 30): datetime(2014, 7, 1, 18, 30),
2204                datetime(2014, 7, 7, 10, 30, 30): datetime(2014, 7, 2, 9, 30, 30),
2205            },
2206        )
2207    )
2208
2209    # 5 days and 3 hours later
2210    apply_large_n_cases.append(
2211        (
2212            BusinessHour(28, start=["21:00", "03:00"], end=["01:00", "04:00"]),
2213            {
2214                datetime(2014, 7, 1, 11): datetime(2014, 7, 9, 0),
2215                datetime(2014, 7, 1, 22): datetime(2014, 7, 9, 3),
2216                datetime(2014, 7, 1, 23): datetime(2014, 7, 9, 21),
2217                datetime(2014, 7, 2, 2): datetime(2014, 7, 9, 23),
2218                datetime(2014, 7, 3, 21): datetime(2014, 7, 11, 0),
2219                datetime(2014, 7, 4, 1): datetime(2014, 7, 11, 23),
2220                datetime(2014, 7, 4, 2): datetime(2014, 7, 11, 23),
2221                datetime(2014, 7, 4, 3): datetime(2014, 7, 11, 23),
2222                datetime(2014, 7, 4, 21): datetime(2014, 7, 12, 0),
2223                datetime(2014, 7, 5, 0): datetime(2014, 7, 14, 22),
2224                datetime(2014, 7, 5, 1): datetime(2014, 7, 14, 23),
2225                datetime(2014, 7, 5, 15): datetime(2014, 7, 14, 23),
2226                datetime(2014, 7, 6, 18): datetime(2014, 7, 14, 23),
2227                datetime(2014, 7, 7, 1): datetime(2014, 7, 14, 23),
2228                datetime(2014, 7, 7, 23, 30): datetime(2014, 7, 15, 21, 30),
2229            },
2230        )
2231    )
2232
2233    @pytest.mark.parametrize("case", apply_large_n_cases)
2234    def test_apply_large_n(self, case):
2235        offset, cases = case
2236        for base, expected in cases.items():
2237            assert_offset_equal(offset, base, expected)
2238
2239    def test_apply_nanoseconds(self):
2240        tests = []
2241
2242        tests.append(
2243            (
2244                BusinessHour(),
2245                {
2246                    Timestamp("2014-07-04 15:00")
2247                    + Nano(5): Timestamp("2014-07-04 16:00")
2248                    + Nano(5),
2249                    Timestamp("2014-07-04 16:00")
2250                    + Nano(5): Timestamp("2014-07-07 09:00")
2251                    + Nano(5),
2252                    Timestamp("2014-07-04 16:00")
2253                    - Nano(5): Timestamp("2014-07-04 17:00")
2254                    - Nano(5),
2255                },
2256            )
2257        )
2258
2259        tests.append(
2260            (
2261                BusinessHour(-1),
2262                {
2263                    Timestamp("2014-07-04 15:00")
2264                    + Nano(5): Timestamp("2014-07-04 14:00")
2265                    + Nano(5),
2266                    Timestamp("2014-07-04 10:00")
2267                    + Nano(5): Timestamp("2014-07-04 09:00")
2268                    + Nano(5),
2269                    Timestamp("2014-07-04 10:00")
2270                    - Nano(5): Timestamp("2014-07-03 17:00")
2271                    - Nano(5),
2272                },
2273            )
2274        )
2275
2276        for offset, cases in tests:
2277            for base, expected in cases.items():
2278                assert_offset_equal(offset, base, expected)
2279
2280    def test_datetimeindex(self):
2281        idx1 = date_range(start="2014-07-04 15:00", end="2014-07-08 10:00", freq="BH")
2282        idx2 = date_range(start="2014-07-04 15:00", periods=12, freq="BH")
2283        idx3 = date_range(end="2014-07-08 10:00", periods=12, freq="BH")
2284        expected = DatetimeIndex(
2285            [
2286                "2014-07-04 15:00",
2287                "2014-07-04 16:00",
2288                "2014-07-07 09:00",
2289                "2014-07-07 10:00",
2290                "2014-07-07 11:00",
2291                "2014-07-07 12:00",
2292                "2014-07-07 13:00",
2293                "2014-07-07 14:00",
2294                "2014-07-07 15:00",
2295                "2014-07-07 16:00",
2296                "2014-07-08 09:00",
2297                "2014-07-08 10:00",
2298            ],
2299            freq="BH",
2300        )
2301        for idx in [idx1, idx2, idx3]:
2302            tm.assert_index_equal(idx, expected)
2303
2304        idx1 = date_range(start="2014-07-04 15:45", end="2014-07-08 10:45", freq="BH")
2305        idx2 = date_range(start="2014-07-04 15:45", periods=12, freq="BH")
2306        idx3 = date_range(end="2014-07-08 10:45", periods=12, freq="BH")
2307
2308        expected = DatetimeIndex(
2309            [
2310                "2014-07-04 15:45",
2311                "2014-07-04 16:45",
2312                "2014-07-07 09:45",
2313                "2014-07-07 10:45",
2314                "2014-07-07 11:45",
2315                "2014-07-07 12:45",
2316                "2014-07-07 13:45",
2317                "2014-07-07 14:45",
2318                "2014-07-07 15:45",
2319                "2014-07-07 16:45",
2320                "2014-07-08 09:45",
2321                "2014-07-08 10:45",
2322            ],
2323            freq="BH",
2324        )
2325        expected = idx1
2326        for idx in [idx1, idx2, idx3]:
2327            tm.assert_index_equal(idx, expected)
2328
2329
2330class TestCustomBusinessHour(Base):
2331    _offset = CustomBusinessHour
2332    holidays = ["2014-06-27", datetime(2014, 6, 30), np.datetime64("2014-07-02")]
2333
2334    def setup_method(self, method):
2335        # 2014 Calendar to check custom holidays
2336        #   Sun Mon Tue Wed Thu Fri Sat
2337        #  6/22  23  24  25  26  27  28
2338        #    29  30 7/1   2   3   4   5
2339        #     6   7   8   9  10  11  12
2340        self.d = datetime(2014, 7, 1, 10, 00)
2341        self.offset1 = CustomBusinessHour(weekmask="Tue Wed Thu Fri")
2342
2343        self.offset2 = CustomBusinessHour(holidays=self.holidays)
2344
2345    def test_constructor_errors(self):
2346        from datetime import time as dt_time
2347
2348        msg = "time data must be specified only with hour and minute"
2349        with pytest.raises(ValueError, match=msg):
2350            CustomBusinessHour(start=dt_time(11, 0, 5))
2351        msg = "time data must match '%H:%M' format"
2352        with pytest.raises(ValueError, match=msg):
2353            CustomBusinessHour(start="AAA")
2354        msg = "time data must match '%H:%M' format"
2355        with pytest.raises(ValueError, match=msg):
2356            CustomBusinessHour(start="14:00:05")
2357
2358    def test_different_normalize_equals(self):
2359        # GH#21404 changed __eq__ to return False when `normalize` does not match
2360        offset = self._offset()
2361        offset2 = self._offset(normalize=True)
2362        assert offset != offset2
2363
2364    def test_repr(self):
2365        assert repr(self.offset1) == "<CustomBusinessHour: CBH=09:00-17:00>"
2366        assert repr(self.offset2) == "<CustomBusinessHour: CBH=09:00-17:00>"
2367
2368    def test_with_offset(self):
2369        expected = Timestamp("2014-07-01 13:00")
2370
2371        assert self.d + CustomBusinessHour() * 3 == expected
2372        assert self.d + CustomBusinessHour(n=3) == expected
2373
2374    def test_eq(self):
2375        for offset in [self.offset1, self.offset2]:
2376            assert offset == offset
2377
2378        assert CustomBusinessHour() != CustomBusinessHour(-1)
2379        assert CustomBusinessHour(start="09:00") == CustomBusinessHour()
2380        assert CustomBusinessHour(start="09:00") != CustomBusinessHour(start="09:01")
2381        assert CustomBusinessHour(start="09:00", end="17:00") != CustomBusinessHour(
2382            start="17:00", end="09:01"
2383        )
2384
2385        assert CustomBusinessHour(weekmask="Tue Wed Thu Fri") != CustomBusinessHour(
2386            weekmask="Mon Tue Wed Thu Fri"
2387        )
2388        assert CustomBusinessHour(holidays=["2014-06-27"]) != CustomBusinessHour(
2389            holidays=["2014-06-28"]
2390        )
2391
2392    def test_sub(self):
2393        # override the Base.test_sub implementation because self.offset2 is
2394        # defined differently in this class than the test expects
2395        pass
2396
2397    def test_hash(self):
2398        assert hash(self.offset1) == hash(self.offset1)
2399        assert hash(self.offset2) == hash(self.offset2)
2400
2401    def test_call(self):
2402        with tm.assert_produces_warning(FutureWarning):
2403            # GH#34171 DateOffset.__call__ is deprecated
2404            assert self.offset1(self.d) == datetime(2014, 7, 1, 11)
2405            assert self.offset2(self.d) == datetime(2014, 7, 1, 11)
2406
2407    def testRollback1(self):
2408        assert self.offset1.rollback(self.d) == self.d
2409        assert self.offset2.rollback(self.d) == self.d
2410
2411        d = datetime(2014, 7, 1, 0)
2412
2413        # 2014/07/01 is Tuesday, 06/30 is Monday(holiday)
2414        assert self.offset1.rollback(d) == datetime(2014, 6, 27, 17)
2415
2416        # 2014/6/30 and 2014/6/27 are holidays
2417        assert self.offset2.rollback(d) == datetime(2014, 6, 26, 17)
2418
2419    def testRollback2(self):
2420        assert self._offset(-3).rollback(datetime(2014, 7, 5, 15, 0)) == datetime(
2421            2014, 7, 4, 17, 0
2422        )
2423
2424    def testRollforward1(self):
2425        assert self.offset1.rollforward(self.d) == self.d
2426        assert self.offset2.rollforward(self.d) == self.d
2427
2428        d = datetime(2014, 7, 1, 0)
2429        assert self.offset1.rollforward(d) == datetime(2014, 7, 1, 9)
2430        assert self.offset2.rollforward(d) == datetime(2014, 7, 1, 9)
2431
2432    def testRollforward2(self):
2433        assert self._offset(-3).rollforward(datetime(2014, 7, 5, 16, 0)) == datetime(
2434            2014, 7, 7, 9
2435        )
2436
2437    def test_roll_date_object(self):
2438        offset = BusinessHour()
2439
2440        dt = datetime(2014, 7, 6, 15, 0)
2441
2442        result = offset.rollback(dt)
2443        assert result == datetime(2014, 7, 4, 17)
2444
2445        result = offset.rollforward(dt)
2446        assert result == datetime(2014, 7, 7, 9)
2447
2448    normalize_cases = []
2449    normalize_cases.append(
2450        (
2451            CustomBusinessHour(normalize=True, holidays=holidays),
2452            {
2453                datetime(2014, 7, 1, 8): datetime(2014, 7, 1),
2454                datetime(2014, 7, 1, 17): datetime(2014, 7, 3),
2455                datetime(2014, 7, 1, 16): datetime(2014, 7, 3),
2456                datetime(2014, 7, 1, 23): datetime(2014, 7, 3),
2457                datetime(2014, 7, 1, 0): datetime(2014, 7, 1),
2458                datetime(2014, 7, 4, 15): datetime(2014, 7, 4),
2459                datetime(2014, 7, 4, 15, 59): datetime(2014, 7, 4),
2460                datetime(2014, 7, 4, 16, 30): datetime(2014, 7, 7),
2461                datetime(2014, 7, 5, 23): datetime(2014, 7, 7),
2462                datetime(2014, 7, 6, 10): datetime(2014, 7, 7),
2463            },
2464        )
2465    )
2466
2467    normalize_cases.append(
2468        (
2469            CustomBusinessHour(-1, normalize=True, holidays=holidays),
2470            {
2471                datetime(2014, 7, 1, 8): datetime(2014, 6, 26),
2472                datetime(2014, 7, 1, 17): datetime(2014, 7, 1),
2473                datetime(2014, 7, 1, 16): datetime(2014, 7, 1),
2474                datetime(2014, 7, 1, 10): datetime(2014, 6, 26),
2475                datetime(2014, 7, 1, 0): datetime(2014, 6, 26),
2476                datetime(2014, 7, 7, 10): datetime(2014, 7, 4),
2477                datetime(2014, 7, 7, 10, 1): datetime(2014, 7, 7),
2478                datetime(2014, 7, 5, 23): datetime(2014, 7, 4),
2479                datetime(2014, 7, 6, 10): datetime(2014, 7, 4),
2480            },
2481        )
2482    )
2483
2484    normalize_cases.append(
2485        (
2486            CustomBusinessHour(
2487                1, normalize=True, start="17:00", end="04:00", holidays=holidays
2488            ),
2489            {
2490                datetime(2014, 7, 1, 8): datetime(2014, 7, 1),
2491                datetime(2014, 7, 1, 17): datetime(2014, 7, 1),
2492                datetime(2014, 7, 1, 23): datetime(2014, 7, 2),
2493                datetime(2014, 7, 2, 2): datetime(2014, 7, 2),
2494                datetime(2014, 7, 2, 3): datetime(2014, 7, 3),
2495                datetime(2014, 7, 4, 23): datetime(2014, 7, 5),
2496                datetime(2014, 7, 5, 2): datetime(2014, 7, 5),
2497                datetime(2014, 7, 7, 2): datetime(2014, 7, 7),
2498                datetime(2014, 7, 7, 17): datetime(2014, 7, 7),
2499            },
2500        )
2501    )
2502
2503    @pytest.mark.parametrize("norm_cases", normalize_cases)
2504    def test_normalize(self, norm_cases):
2505        offset, cases = norm_cases
2506        for dt, expected in cases.items():
2507            assert offset.apply(dt) == expected
2508
2509    def test_is_on_offset(self):
2510        tests = []
2511
2512        tests.append(
2513            (
2514                CustomBusinessHour(start="10:00", end="15:00", holidays=self.holidays),
2515                {
2516                    datetime(2014, 7, 1, 9): False,
2517                    datetime(2014, 7, 1, 10): True,
2518                    datetime(2014, 7, 1, 15): True,
2519                    datetime(2014, 7, 1, 15, 1): False,
2520                    datetime(2014, 7, 5, 12): False,
2521                    datetime(2014, 7, 6, 12): False,
2522                },
2523            )
2524        )
2525
2526        for offset, cases in tests:
2527            for dt, expected in cases.items():
2528                assert offset.is_on_offset(dt) == expected
2529
2530    apply_cases = []
2531    apply_cases.append(
2532        (
2533            CustomBusinessHour(holidays=holidays),
2534            {
2535                datetime(2014, 7, 1, 11): datetime(2014, 7, 1, 12),
2536                datetime(2014, 7, 1, 13): datetime(2014, 7, 1, 14),
2537                datetime(2014, 7, 1, 15): datetime(2014, 7, 1, 16),
2538                datetime(2014, 7, 1, 19): datetime(2014, 7, 3, 10),
2539                datetime(2014, 7, 1, 16): datetime(2014, 7, 3, 9),
2540                datetime(2014, 7, 1, 16, 30, 15): datetime(2014, 7, 3, 9, 30, 15),
2541                datetime(2014, 7, 1, 17): datetime(2014, 7, 3, 10),
2542                datetime(2014, 7, 2, 11): datetime(2014, 7, 3, 10),
2543                # out of business hours
2544                datetime(2014, 7, 2, 8): datetime(2014, 7, 3, 10),
2545                datetime(2014, 7, 2, 19): datetime(2014, 7, 3, 10),
2546                datetime(2014, 7, 2, 23): datetime(2014, 7, 3, 10),
2547                datetime(2014, 7, 3, 0): datetime(2014, 7, 3, 10),
2548                # saturday
2549                datetime(2014, 7, 5, 15): datetime(2014, 7, 7, 10),
2550                datetime(2014, 7, 4, 17): datetime(2014, 7, 7, 10),
2551                datetime(2014, 7, 4, 16, 30): datetime(2014, 7, 7, 9, 30),
2552                datetime(2014, 7, 4, 16, 30, 30): datetime(2014, 7, 7, 9, 30, 30),
2553            },
2554        )
2555    )
2556
2557    apply_cases.append(
2558        (
2559            CustomBusinessHour(4, holidays=holidays),
2560            {
2561                datetime(2014, 7, 1, 11): datetime(2014, 7, 1, 15),
2562                datetime(2014, 7, 1, 13): datetime(2014, 7, 3, 9),
2563                datetime(2014, 7, 1, 15): datetime(2014, 7, 3, 11),
2564                datetime(2014, 7, 1, 16): datetime(2014, 7, 3, 12),
2565                datetime(2014, 7, 1, 17): datetime(2014, 7, 3, 13),
2566                datetime(2014, 7, 2, 11): datetime(2014, 7, 3, 13),
2567                datetime(2014, 7, 2, 8): datetime(2014, 7, 3, 13),
2568                datetime(2014, 7, 2, 19): datetime(2014, 7, 3, 13),
2569                datetime(2014, 7, 2, 23): datetime(2014, 7, 3, 13),
2570                datetime(2014, 7, 3, 0): datetime(2014, 7, 3, 13),
2571                datetime(2014, 7, 5, 15): datetime(2014, 7, 7, 13),
2572                datetime(2014, 7, 4, 17): datetime(2014, 7, 7, 13),
2573                datetime(2014, 7, 4, 16, 30): datetime(2014, 7, 7, 12, 30),
2574                datetime(2014, 7, 4, 16, 30, 30): datetime(2014, 7, 7, 12, 30, 30),
2575            },
2576        )
2577    )
2578
2579    @pytest.mark.parametrize("apply_case", apply_cases)
2580    def test_apply(self, apply_case):
2581        offset, cases = apply_case
2582        for base, expected in cases.items():
2583            assert_offset_equal(offset, base, expected)
2584
2585    nano_cases = []
2586    nano_cases.append(
2587        (
2588            CustomBusinessHour(holidays=holidays),
2589            {
2590                Timestamp("2014-07-01 15:00")
2591                + Nano(5): Timestamp("2014-07-01 16:00")
2592                + Nano(5),
2593                Timestamp("2014-07-01 16:00")
2594                + Nano(5): Timestamp("2014-07-03 09:00")
2595                + Nano(5),
2596                Timestamp("2014-07-01 16:00")
2597                - Nano(5): Timestamp("2014-07-01 17:00")
2598                - Nano(5),
2599            },
2600        )
2601    )
2602
2603    nano_cases.append(
2604        (
2605            CustomBusinessHour(-1, holidays=holidays),
2606            {
2607                Timestamp("2014-07-01 15:00")
2608                + Nano(5): Timestamp("2014-07-01 14:00")
2609                + Nano(5),
2610                Timestamp("2014-07-01 10:00")
2611                + Nano(5): Timestamp("2014-07-01 09:00")
2612                + Nano(5),
2613                Timestamp("2014-07-01 10:00")
2614                - Nano(5): Timestamp("2014-06-26 17:00")
2615                - Nano(5),
2616            },
2617        )
2618    )
2619
2620    @pytest.mark.parametrize("nano_case", nano_cases)
2621    def test_apply_nanoseconds(self, nano_case):
2622        offset, cases = nano_case
2623        for base, expected in cases.items():
2624            assert_offset_equal(offset, base, expected)
2625
2626
2627class TestCustomBusinessDay(Base):
2628    _offset = CDay
2629
2630    def setup_method(self, method):
2631        self.d = datetime(2008, 1, 1)
2632        self.nd = np_datetime64_compat("2008-01-01 00:00:00Z")
2633
2634        self.offset = CDay()
2635        self.offset1 = self.offset
2636        self.offset2 = CDay(2)
2637
2638    def test_different_normalize_equals(self):
2639        # GH#21404 changed __eq__ to return False when `normalize` does not match
2640        offset = self._offset()
2641        offset2 = self._offset(normalize=True)
2642        assert offset != offset2
2643
2644    def test_repr(self):
2645        assert repr(self.offset) == "<CustomBusinessDay>"
2646        assert repr(self.offset2) == "<2 * CustomBusinessDays>"
2647
2648        expected = "<BusinessDay: offset=datetime.timedelta(days=1)>"
2649        assert repr(self.offset + timedelta(1)) == expected
2650
2651    def test_with_offset(self):
2652        offset = self.offset + timedelta(hours=2)
2653
2654        assert (self.d + offset) == datetime(2008, 1, 2, 2)
2655
2656    def test_with_offset_index(self):
2657        dti = DatetimeIndex([self.d])
2658        result = dti + (self.offset + timedelta(hours=2))
2659
2660        expected = DatetimeIndex([datetime(2008, 1, 2, 2)])
2661        tm.assert_index_equal(result, expected)
2662
2663    def test_eq(self):
2664        assert self.offset2 == self.offset2
2665
2666    def test_mul(self):
2667        pass
2668
2669    def test_hash(self):
2670        assert hash(self.offset2) == hash(self.offset2)
2671
2672    def test_call(self):
2673        with tm.assert_produces_warning(FutureWarning):
2674            # GH#34171 DateOffset.__call__ is deprecated
2675            assert self.offset2(self.d) == datetime(2008, 1, 3)
2676            assert self.offset2(self.nd) == datetime(2008, 1, 3)
2677
2678    def testRollback1(self):
2679        assert CDay(10).rollback(self.d) == self.d
2680
2681    def testRollback2(self):
2682        assert CDay(10).rollback(datetime(2008, 1, 5)) == datetime(2008, 1, 4)
2683
2684    def testRollforward1(self):
2685        assert CDay(10).rollforward(self.d) == self.d
2686
2687    def testRollforward2(self):
2688        assert CDay(10).rollforward(datetime(2008, 1, 5)) == datetime(2008, 1, 7)
2689
2690    def test_roll_date_object(self):
2691        offset = CDay()
2692
2693        dt = date(2012, 9, 15)
2694
2695        result = offset.rollback(dt)
2696        assert result == datetime(2012, 9, 14)
2697
2698        result = offset.rollforward(dt)
2699        assert result == datetime(2012, 9, 17)
2700
2701        offset = offsets.Day()
2702        result = offset.rollback(dt)
2703        assert result == datetime(2012, 9, 15)
2704
2705        result = offset.rollforward(dt)
2706        assert result == datetime(2012, 9, 15)
2707
2708    on_offset_cases = [
2709        (CDay(), datetime(2008, 1, 1), True),
2710        (CDay(), datetime(2008, 1, 5), False),
2711    ]
2712
2713    @pytest.mark.parametrize("case", on_offset_cases)
2714    def test_is_on_offset(self, case):
2715        offset, d, expected = case
2716        assert_is_on_offset(offset, d, expected)
2717
2718    apply_cases: _ApplyCases = []
2719    apply_cases.append(
2720        (
2721            CDay(),
2722            {
2723                datetime(2008, 1, 1): datetime(2008, 1, 2),
2724                datetime(2008, 1, 4): datetime(2008, 1, 7),
2725                datetime(2008, 1, 5): datetime(2008, 1, 7),
2726                datetime(2008, 1, 6): datetime(2008, 1, 7),
2727                datetime(2008, 1, 7): datetime(2008, 1, 8),
2728            },
2729        )
2730    )
2731
2732    apply_cases.append(
2733        (
2734            2 * CDay(),
2735            {
2736                datetime(2008, 1, 1): datetime(2008, 1, 3),
2737                datetime(2008, 1, 4): datetime(2008, 1, 8),
2738                datetime(2008, 1, 5): datetime(2008, 1, 8),
2739                datetime(2008, 1, 6): datetime(2008, 1, 8),
2740                datetime(2008, 1, 7): datetime(2008, 1, 9),
2741            },
2742        )
2743    )
2744
2745    apply_cases.append(
2746        (
2747            -CDay(),
2748            {
2749                datetime(2008, 1, 1): datetime(2007, 12, 31),
2750                datetime(2008, 1, 4): datetime(2008, 1, 3),
2751                datetime(2008, 1, 5): datetime(2008, 1, 4),
2752                datetime(2008, 1, 6): datetime(2008, 1, 4),
2753                datetime(2008, 1, 7): datetime(2008, 1, 4),
2754                datetime(2008, 1, 8): datetime(2008, 1, 7),
2755            },
2756        )
2757    )
2758
2759    apply_cases.append(
2760        (
2761            -2 * CDay(),
2762            {
2763                datetime(2008, 1, 1): datetime(2007, 12, 28),
2764                datetime(2008, 1, 4): datetime(2008, 1, 2),
2765                datetime(2008, 1, 5): datetime(2008, 1, 3),
2766                datetime(2008, 1, 6): datetime(2008, 1, 3),
2767                datetime(2008, 1, 7): datetime(2008, 1, 3),
2768                datetime(2008, 1, 8): datetime(2008, 1, 4),
2769                datetime(2008, 1, 9): datetime(2008, 1, 7),
2770            },
2771        )
2772    )
2773
2774    apply_cases.append(
2775        (
2776            CDay(0),
2777            {
2778                datetime(2008, 1, 1): datetime(2008, 1, 1),
2779                datetime(2008, 1, 4): datetime(2008, 1, 4),
2780                datetime(2008, 1, 5): datetime(2008, 1, 7),
2781                datetime(2008, 1, 6): datetime(2008, 1, 7),
2782                datetime(2008, 1, 7): datetime(2008, 1, 7),
2783            },
2784        )
2785    )
2786
2787    @pytest.mark.parametrize("case", apply_cases)
2788    def test_apply(self, case):
2789        offset, cases = case
2790        for base, expected in cases.items():
2791            assert_offset_equal(offset, base, expected)
2792
2793    def test_apply_large_n(self):
2794        dt = datetime(2012, 10, 23)
2795
2796        result = dt + CDay(10)
2797        assert result == datetime(2012, 11, 6)
2798
2799        result = dt + CDay(100) - CDay(100)
2800        assert result == dt
2801
2802        off = CDay() * 6
2803        rs = datetime(2012, 1, 1) - off
2804        xp = datetime(2011, 12, 23)
2805        assert rs == xp
2806
2807        st = datetime(2011, 12, 18)
2808        rs = st + off
2809        xp = datetime(2011, 12, 26)
2810        assert rs == xp
2811
2812    def test_apply_corner(self):
2813        msg = (
2814            "Only know how to combine trading day "
2815            "with datetime, datetime64 or timedelta"
2816        )
2817        with pytest.raises(ApplyTypeError, match=msg):
2818            CDay().apply(BMonthEnd())
2819
2820    def test_holidays(self):
2821        # Define a TradingDay offset
2822        holidays = ["2012-05-01", datetime(2013, 5, 1), np.datetime64("2014-05-01")]
2823        tday = CDay(holidays=holidays)
2824        for year in range(2012, 2015):
2825            dt = datetime(year, 4, 30)
2826            xp = datetime(year, 5, 2)
2827            rs = dt + tday
2828            assert rs == xp
2829
2830    def test_weekmask(self):
2831        weekmask_saudi = "Sat Sun Mon Tue Wed"  # Thu-Fri Weekend
2832        weekmask_uae = "1111001"  # Fri-Sat Weekend
2833        weekmask_egypt = [1, 1, 1, 1, 0, 0, 1]  # Fri-Sat Weekend
2834        bday_saudi = CDay(weekmask=weekmask_saudi)
2835        bday_uae = CDay(weekmask=weekmask_uae)
2836        bday_egypt = CDay(weekmask=weekmask_egypt)
2837        dt = datetime(2013, 5, 1)
2838        xp_saudi = datetime(2013, 5, 4)
2839        xp_uae = datetime(2013, 5, 2)
2840        xp_egypt = datetime(2013, 5, 2)
2841        assert xp_saudi == dt + bday_saudi
2842        assert xp_uae == dt + bday_uae
2843        assert xp_egypt == dt + bday_egypt
2844        xp2 = datetime(2013, 5, 5)
2845        assert xp2 == dt + 2 * bday_saudi
2846        assert xp2 == dt + 2 * bday_uae
2847        assert xp2 == dt + 2 * bday_egypt
2848
2849    def test_weekmask_and_holidays(self):
2850        weekmask_egypt = "Sun Mon Tue Wed Thu"  # Fri-Sat Weekend
2851        holidays = ["2012-05-01", datetime(2013, 5, 1), np.datetime64("2014-05-01")]
2852        bday_egypt = CDay(holidays=holidays, weekmask=weekmask_egypt)
2853        dt = datetime(2013, 4, 30)
2854        xp_egypt = datetime(2013, 5, 5)
2855        assert xp_egypt == dt + 2 * bday_egypt
2856
2857    @pytest.mark.filterwarnings("ignore:Non:pandas.errors.PerformanceWarning")
2858    def test_calendar(self):
2859        calendar = USFederalHolidayCalendar()
2860        dt = datetime(2014, 1, 17)
2861        assert_offset_equal(CDay(calendar=calendar), dt, datetime(2014, 1, 21))
2862
2863    def test_roundtrip_pickle(self):
2864        def _check_roundtrip(obj):
2865            unpickled = tm.round_trip_pickle(obj)
2866            assert unpickled == obj
2867
2868        _check_roundtrip(self.offset)
2869        _check_roundtrip(self.offset2)
2870        _check_roundtrip(self.offset * 2)
2871
2872    def test_pickle_compat_0_14_1(self, datapath):
2873        hdays = [datetime(2013, 1, 1) for ele in range(4)]
2874        pth = datapath("tseries", "offsets", "data", "cday-0.14.1.pickle")
2875        cday0_14_1 = read_pickle(pth)
2876        cday = CDay(holidays=hdays)
2877        assert cday == cday0_14_1
2878
2879
2880class CustomBusinessMonthBase:
2881    def setup_method(self, method):
2882        self.d = datetime(2008, 1, 1)
2883
2884        self.offset = self._offset()
2885        self.offset1 = self.offset
2886        self.offset2 = self._offset(2)
2887
2888    def test_eq(self):
2889        assert self.offset2 == self.offset2
2890
2891    def test_mul(self):
2892        pass
2893
2894    def test_hash(self):
2895        assert hash(self.offset2) == hash(self.offset2)
2896
2897    def test_roundtrip_pickle(self):
2898        def _check_roundtrip(obj):
2899            unpickled = tm.round_trip_pickle(obj)
2900            assert unpickled == obj
2901
2902        _check_roundtrip(self._offset())
2903        _check_roundtrip(self._offset(2))
2904        _check_roundtrip(self._offset() * 2)
2905
2906    def test_copy(self):
2907        # GH 17452
2908        off = self._offset(weekmask="Mon Wed Fri")
2909        assert off == off.copy()
2910
2911
2912class TestCustomBusinessMonthEnd(CustomBusinessMonthBase, Base):
2913    _offset = CBMonthEnd
2914
2915    def test_different_normalize_equals(self):
2916        # GH#21404 changed __eq__ to return False when `normalize` does not match
2917        offset = self._offset()
2918        offset2 = self._offset(normalize=True)
2919        assert offset != offset2
2920
2921    def test_repr(self):
2922        assert repr(self.offset) == "<CustomBusinessMonthEnd>"
2923        assert repr(self.offset2) == "<2 * CustomBusinessMonthEnds>"
2924
2925    def test_call(self):
2926        with tm.assert_produces_warning(FutureWarning):
2927            # GH#34171 DateOffset.__call__ is deprecated
2928            assert self.offset2(self.d) == datetime(2008, 2, 29)
2929
2930    def testRollback1(self):
2931        assert CDay(10).rollback(datetime(2007, 12, 31)) == datetime(2007, 12, 31)
2932
2933    def testRollback2(self):
2934        assert CBMonthEnd(10).rollback(self.d) == datetime(2007, 12, 31)
2935
2936    def testRollforward1(self):
2937        assert CBMonthEnd(10).rollforward(self.d) == datetime(2008, 1, 31)
2938
2939    def test_roll_date_object(self):
2940        offset = CBMonthEnd()
2941
2942        dt = date(2012, 9, 15)
2943
2944        result = offset.rollback(dt)
2945        assert result == datetime(2012, 8, 31)
2946
2947        result = offset.rollforward(dt)
2948        assert result == datetime(2012, 9, 28)
2949
2950        offset = offsets.Day()
2951        result = offset.rollback(dt)
2952        assert result == datetime(2012, 9, 15)
2953
2954        result = offset.rollforward(dt)
2955        assert result == datetime(2012, 9, 15)
2956
2957    on_offset_cases = [
2958        (CBMonthEnd(), datetime(2008, 1, 31), True),
2959        (CBMonthEnd(), datetime(2008, 1, 1), False),
2960    ]
2961
2962    @pytest.mark.parametrize("case", on_offset_cases)
2963    def test_is_on_offset(self, case):
2964        offset, d, expected = case
2965        assert_is_on_offset(offset, d, expected)
2966
2967    apply_cases: _ApplyCases = []
2968    apply_cases.append(
2969        (
2970            CBMonthEnd(),
2971            {
2972                datetime(2008, 1, 1): datetime(2008, 1, 31),
2973                datetime(2008, 2, 7): datetime(2008, 2, 29),
2974            },
2975        )
2976    )
2977
2978    apply_cases.append(
2979        (
2980            2 * CBMonthEnd(),
2981            {
2982                datetime(2008, 1, 1): datetime(2008, 2, 29),
2983                datetime(2008, 2, 7): datetime(2008, 3, 31),
2984            },
2985        )
2986    )
2987
2988    apply_cases.append(
2989        (
2990            -CBMonthEnd(),
2991            {
2992                datetime(2008, 1, 1): datetime(2007, 12, 31),
2993                datetime(2008, 2, 8): datetime(2008, 1, 31),
2994            },
2995        )
2996    )
2997
2998    apply_cases.append(
2999        (
3000            -2 * CBMonthEnd(),
3001            {
3002                datetime(2008, 1, 1): datetime(2007, 11, 30),
3003                datetime(2008, 2, 9): datetime(2007, 12, 31),
3004            },
3005        )
3006    )
3007
3008    apply_cases.append(
3009        (
3010            CBMonthEnd(0),
3011            {
3012                datetime(2008, 1, 1): datetime(2008, 1, 31),
3013                datetime(2008, 2, 7): datetime(2008, 2, 29),
3014            },
3015        )
3016    )
3017
3018    @pytest.mark.parametrize("case", apply_cases)
3019    def test_apply(self, case):
3020        offset, cases = case
3021        for base, expected in cases.items():
3022            assert_offset_equal(offset, base, expected)
3023
3024    def test_apply_large_n(self):
3025        dt = datetime(2012, 10, 23)
3026
3027        result = dt + CBMonthEnd(10)
3028        assert result == datetime(2013, 7, 31)
3029
3030        result = dt + CDay(100) - CDay(100)
3031        assert result == dt
3032
3033        off = CBMonthEnd() * 6
3034        rs = datetime(2012, 1, 1) - off
3035        xp = datetime(2011, 7, 29)
3036        assert rs == xp
3037
3038        st = datetime(2011, 12, 18)
3039        rs = st + off
3040        xp = datetime(2012, 5, 31)
3041        assert rs == xp
3042
3043    def test_holidays(self):
3044        # Define a TradingDay offset
3045        holidays = ["2012-01-31", datetime(2012, 2, 28), np.datetime64("2012-02-29")]
3046        bm_offset = CBMonthEnd(holidays=holidays)
3047        dt = datetime(2012, 1, 1)
3048        assert dt + bm_offset == datetime(2012, 1, 30)
3049        assert dt + 2 * bm_offset == datetime(2012, 2, 27)
3050
3051    @pytest.mark.filterwarnings("ignore:Non:pandas.errors.PerformanceWarning")
3052    def test_datetimeindex(self):
3053        from pandas.tseries.holiday import USFederalHolidayCalendar
3054
3055        hcal = USFederalHolidayCalendar()
3056        freq = CBMonthEnd(calendar=hcal)
3057
3058        assert date_range(start="20120101", end="20130101", freq=freq).tolist()[
3059            0
3060        ] == datetime(2012, 1, 31)
3061
3062
3063class TestCustomBusinessMonthBegin(CustomBusinessMonthBase, Base):
3064    _offset = CBMonthBegin
3065
3066    def test_different_normalize_equals(self):
3067        # GH#21404 changed __eq__ to return False when `normalize` does not match
3068        offset = self._offset()
3069        offset2 = self._offset(normalize=True)
3070        assert offset != offset2
3071
3072    def test_repr(self):
3073        assert repr(self.offset) == "<CustomBusinessMonthBegin>"
3074        assert repr(self.offset2) == "<2 * CustomBusinessMonthBegins>"
3075
3076    def test_call(self):
3077        with tm.assert_produces_warning(FutureWarning):
3078            # GH#34171 DateOffset.__call__ is deprecated
3079            assert self.offset2(self.d) == datetime(2008, 3, 3)
3080
3081    def testRollback1(self):
3082        assert CDay(10).rollback(datetime(2007, 12, 31)) == datetime(2007, 12, 31)
3083
3084    def testRollback2(self):
3085        assert CBMonthBegin(10).rollback(self.d) == datetime(2008, 1, 1)
3086
3087    def testRollforward1(self):
3088        assert CBMonthBegin(10).rollforward(self.d) == datetime(2008, 1, 1)
3089
3090    def test_roll_date_object(self):
3091        offset = CBMonthBegin()
3092
3093        dt = date(2012, 9, 15)
3094
3095        result = offset.rollback(dt)
3096        assert result == datetime(2012, 9, 3)
3097
3098        result = offset.rollforward(dt)
3099        assert result == datetime(2012, 10, 1)
3100
3101        offset = offsets.Day()
3102        result = offset.rollback(dt)
3103        assert result == datetime(2012, 9, 15)
3104
3105        result = offset.rollforward(dt)
3106        assert result == datetime(2012, 9, 15)
3107
3108    on_offset_cases = [
3109        (CBMonthBegin(), datetime(2008, 1, 1), True),
3110        (CBMonthBegin(), datetime(2008, 1, 31), False),
3111    ]
3112
3113    @pytest.mark.parametrize("case", on_offset_cases)
3114    def test_is_on_offset(self, case):
3115        offset, dt, expected = case
3116        assert_is_on_offset(offset, dt, expected)
3117
3118    apply_cases: _ApplyCases = []
3119    apply_cases.append(
3120        (
3121            CBMonthBegin(),
3122            {
3123                datetime(2008, 1, 1): datetime(2008, 2, 1),
3124                datetime(2008, 2, 7): datetime(2008, 3, 3),
3125            },
3126        )
3127    )
3128
3129    apply_cases.append(
3130        (
3131            2 * CBMonthBegin(),
3132            {
3133                datetime(2008, 1, 1): datetime(2008, 3, 3),
3134                datetime(2008, 2, 7): datetime(2008, 4, 1),
3135            },
3136        )
3137    )
3138
3139    apply_cases.append(
3140        (
3141            -CBMonthBegin(),
3142            {
3143                datetime(2008, 1, 1): datetime(2007, 12, 3),
3144                datetime(2008, 2, 8): datetime(2008, 2, 1),
3145            },
3146        )
3147    )
3148
3149    apply_cases.append(
3150        (
3151            -2 * CBMonthBegin(),
3152            {
3153                datetime(2008, 1, 1): datetime(2007, 11, 1),
3154                datetime(2008, 2, 9): datetime(2008, 1, 1),
3155            },
3156        )
3157    )
3158
3159    apply_cases.append(
3160        (
3161            CBMonthBegin(0),
3162            {
3163                datetime(2008, 1, 1): datetime(2008, 1, 1),
3164                datetime(2008, 1, 7): datetime(2008, 2, 1),
3165            },
3166        )
3167    )
3168
3169    @pytest.mark.parametrize("case", apply_cases)
3170    def test_apply(self, case):
3171        offset, cases = case
3172        for base, expected in cases.items():
3173            assert_offset_equal(offset, base, expected)
3174
3175    def test_apply_large_n(self):
3176        dt = datetime(2012, 10, 23)
3177
3178        result = dt + CBMonthBegin(10)
3179        assert result == datetime(2013, 8, 1)
3180
3181        result = dt + CDay(100) - CDay(100)
3182        assert result == dt
3183
3184        off = CBMonthBegin() * 6
3185        rs = datetime(2012, 1, 1) - off
3186        xp = datetime(2011, 7, 1)
3187        assert rs == xp
3188
3189        st = datetime(2011, 12, 18)
3190        rs = st + off
3191
3192        xp = datetime(2012, 6, 1)
3193        assert rs == xp
3194
3195    def test_holidays(self):
3196        # Define a TradingDay offset
3197        holidays = ["2012-02-01", datetime(2012, 2, 2), np.datetime64("2012-03-01")]
3198        bm_offset = CBMonthBegin(holidays=holidays)
3199        dt = datetime(2012, 1, 1)
3200
3201        assert dt + bm_offset == datetime(2012, 1, 2)
3202        assert dt + 2 * bm_offset == datetime(2012, 2, 3)
3203
3204    @pytest.mark.filterwarnings("ignore:Non:pandas.errors.PerformanceWarning")
3205    def test_datetimeindex(self):
3206        hcal = USFederalHolidayCalendar()
3207        cbmb = CBMonthBegin(calendar=hcal)
3208        assert date_range(start="20120101", end="20130101", freq=cbmb).tolist()[
3209            0
3210        ] == datetime(2012, 1, 3)
3211
3212
3213class TestWeek(Base):
3214    _offset = Week
3215    d = Timestamp(datetime(2008, 1, 2))
3216    offset1 = _offset()
3217    offset2 = _offset(2)
3218
3219    def test_repr(self):
3220        assert repr(Week(weekday=0)) == "<Week: weekday=0>"
3221        assert repr(Week(n=-1, weekday=0)) == "<-1 * Week: weekday=0>"
3222        assert repr(Week(n=-2, weekday=0)) == "<-2 * Weeks: weekday=0>"
3223
3224    def test_corner(self):
3225        with pytest.raises(ValueError, match="Day must be"):
3226            Week(weekday=7)
3227
3228        with pytest.raises(ValueError, match="Day must be"):
3229            Week(weekday=-1)
3230
3231    def test_is_anchored(self):
3232        assert Week(weekday=0).is_anchored()
3233        assert not Week().is_anchored()
3234        assert not Week(2, weekday=2).is_anchored()
3235        assert not Week(2).is_anchored()
3236
3237    offset_cases = []
3238    # not business week
3239    offset_cases.append(
3240        (
3241            Week(),
3242            {
3243                datetime(2008, 1, 1): datetime(2008, 1, 8),
3244                datetime(2008, 1, 4): datetime(2008, 1, 11),
3245                datetime(2008, 1, 5): datetime(2008, 1, 12),
3246                datetime(2008, 1, 6): datetime(2008, 1, 13),
3247                datetime(2008, 1, 7): datetime(2008, 1, 14),
3248            },
3249        )
3250    )
3251
3252    # Mon
3253    offset_cases.append(
3254        (
3255            Week(weekday=0),
3256            {
3257                datetime(2007, 12, 31): datetime(2008, 1, 7),
3258                datetime(2008, 1, 4): datetime(2008, 1, 7),
3259                datetime(2008, 1, 5): datetime(2008, 1, 7),
3260                datetime(2008, 1, 6): datetime(2008, 1, 7),
3261                datetime(2008, 1, 7): datetime(2008, 1, 14),
3262            },
3263        )
3264    )
3265
3266    # n=0 -> roll forward. Mon
3267    offset_cases.append(
3268        (
3269            Week(0, weekday=0),
3270            {
3271                datetime(2007, 12, 31): datetime(2007, 12, 31),
3272                datetime(2008, 1, 4): datetime(2008, 1, 7),
3273                datetime(2008, 1, 5): datetime(2008, 1, 7),
3274                datetime(2008, 1, 6): datetime(2008, 1, 7),
3275                datetime(2008, 1, 7): datetime(2008, 1, 7),
3276            },
3277        )
3278    )
3279
3280    # n=0 -> roll forward. Mon
3281    offset_cases.append(
3282        (
3283            Week(-2, weekday=1),
3284            {
3285                datetime(2010, 4, 6): datetime(2010, 3, 23),
3286                datetime(2010, 4, 8): datetime(2010, 3, 30),
3287                datetime(2010, 4, 5): datetime(2010, 3, 23),
3288            },
3289        )
3290    )
3291
3292    @pytest.mark.parametrize("case", offset_cases)
3293    def test_offset(self, case):
3294        offset, cases = case
3295        for base, expected in cases.items():
3296            assert_offset_equal(offset, base, expected)
3297
3298    @pytest.mark.parametrize("weekday", range(7))
3299    def test_is_on_offset(self, weekday):
3300        offset = Week(weekday=weekday)
3301
3302        for day in range(1, 8):
3303            date = datetime(2008, 1, day)
3304
3305            if day % 7 == weekday:
3306                expected = True
3307            else:
3308                expected = False
3309        assert_is_on_offset(offset, date, expected)
3310
3311
3312class TestWeekOfMonth(Base):
3313    _offset = WeekOfMonth
3314    offset1 = _offset()
3315    offset2 = _offset(2)
3316
3317    def test_constructor(self):
3318        with pytest.raises(ValueError, match="^Week"):
3319            WeekOfMonth(n=1, week=4, weekday=0)
3320
3321        with pytest.raises(ValueError, match="^Week"):
3322            WeekOfMonth(n=1, week=-1, weekday=0)
3323
3324        with pytest.raises(ValueError, match="^Day"):
3325            WeekOfMonth(n=1, week=0, weekday=-1)
3326
3327        with pytest.raises(ValueError, match="^Day"):
3328            WeekOfMonth(n=1, week=0, weekday=-7)
3329
3330    def test_repr(self):
3331        assert (
3332            repr(WeekOfMonth(weekday=1, week=2)) == "<WeekOfMonth: week=2, weekday=1>"
3333        )
3334
3335    def test_offset(self):
3336        date1 = datetime(2011, 1, 4)  # 1st Tuesday of Month
3337        date2 = datetime(2011, 1, 11)  # 2nd Tuesday of Month
3338        date3 = datetime(2011, 1, 18)  # 3rd Tuesday of Month
3339        date4 = datetime(2011, 1, 25)  # 4th Tuesday of Month
3340
3341        # see for loop for structure
3342        test_cases = [
3343            (-2, 2, 1, date1, datetime(2010, 11, 16)),
3344            (-2, 2, 1, date2, datetime(2010, 11, 16)),
3345            (-2, 2, 1, date3, datetime(2010, 11, 16)),
3346            (-2, 2, 1, date4, datetime(2010, 12, 21)),
3347            (-1, 2, 1, date1, datetime(2010, 12, 21)),
3348            (-1, 2, 1, date2, datetime(2010, 12, 21)),
3349            (-1, 2, 1, date3, datetime(2010, 12, 21)),
3350            (-1, 2, 1, date4, datetime(2011, 1, 18)),
3351            (0, 0, 1, date1, datetime(2011, 1, 4)),
3352            (0, 0, 1, date2, datetime(2011, 2, 1)),
3353            (0, 0, 1, date3, datetime(2011, 2, 1)),
3354            (0, 0, 1, date4, datetime(2011, 2, 1)),
3355            (0, 1, 1, date1, datetime(2011, 1, 11)),
3356            (0, 1, 1, date2, datetime(2011, 1, 11)),
3357            (0, 1, 1, date3, datetime(2011, 2, 8)),
3358            (0, 1, 1, date4, datetime(2011, 2, 8)),
3359            (0, 0, 1, date1, datetime(2011, 1, 4)),
3360            (0, 1, 1, date2, datetime(2011, 1, 11)),
3361            (0, 2, 1, date3, datetime(2011, 1, 18)),
3362            (0, 3, 1, date4, datetime(2011, 1, 25)),
3363            (1, 0, 0, date1, datetime(2011, 2, 7)),
3364            (1, 0, 0, date2, datetime(2011, 2, 7)),
3365            (1, 0, 0, date3, datetime(2011, 2, 7)),
3366            (1, 0, 0, date4, datetime(2011, 2, 7)),
3367            (1, 0, 1, date1, datetime(2011, 2, 1)),
3368            (1, 0, 1, date2, datetime(2011, 2, 1)),
3369            (1, 0, 1, date3, datetime(2011, 2, 1)),
3370            (1, 0, 1, date4, datetime(2011, 2, 1)),
3371            (1, 0, 2, date1, datetime(2011, 1, 5)),
3372            (1, 0, 2, date2, datetime(2011, 2, 2)),
3373            (1, 0, 2, date3, datetime(2011, 2, 2)),
3374            (1, 0, 2, date4, datetime(2011, 2, 2)),
3375            (1, 2, 1, date1, datetime(2011, 1, 18)),
3376            (1, 2, 1, date2, datetime(2011, 1, 18)),
3377            (1, 2, 1, date3, datetime(2011, 2, 15)),
3378            (1, 2, 1, date4, datetime(2011, 2, 15)),
3379            (2, 2, 1, date1, datetime(2011, 2, 15)),
3380            (2, 2, 1, date2, datetime(2011, 2, 15)),
3381            (2, 2, 1, date3, datetime(2011, 3, 15)),
3382            (2, 2, 1, date4, datetime(2011, 3, 15)),
3383        ]
3384
3385        for n, week, weekday, dt, expected in test_cases:
3386            offset = WeekOfMonth(n, week=week, weekday=weekday)
3387            assert_offset_equal(offset, dt, expected)
3388
3389        # try subtracting
3390        result = datetime(2011, 2, 1) - WeekOfMonth(week=1, weekday=2)
3391        assert result == datetime(2011, 1, 12)
3392
3393        result = datetime(2011, 2, 3) - WeekOfMonth(week=0, weekday=2)
3394        assert result == datetime(2011, 2, 2)
3395
3396    on_offset_cases = [
3397        (0, 0, datetime(2011, 2, 7), True),
3398        (0, 0, datetime(2011, 2, 6), False),
3399        (0, 0, datetime(2011, 2, 14), False),
3400        (1, 0, datetime(2011, 2, 14), True),
3401        (0, 1, datetime(2011, 2, 1), True),
3402        (0, 1, datetime(2011, 2, 8), False),
3403    ]
3404
3405    @pytest.mark.parametrize("case", on_offset_cases)
3406    def test_is_on_offset(self, case):
3407        week, weekday, dt, expected = case
3408        offset = WeekOfMonth(week=week, weekday=weekday)
3409        assert offset.is_on_offset(dt) == expected
3410
3411
3412class TestLastWeekOfMonth(Base):
3413    _offset = LastWeekOfMonth
3414    offset1 = _offset()
3415    offset2 = _offset(2)
3416
3417    def test_constructor(self):
3418        with pytest.raises(ValueError, match="^N cannot be 0"):
3419            LastWeekOfMonth(n=0, weekday=1)
3420
3421        with pytest.raises(ValueError, match="^Day"):
3422            LastWeekOfMonth(n=1, weekday=-1)
3423
3424        with pytest.raises(ValueError, match="^Day"):
3425            LastWeekOfMonth(n=1, weekday=7)
3426
3427    def test_offset(self):
3428        # Saturday
3429        last_sat = datetime(2013, 8, 31)
3430        next_sat = datetime(2013, 9, 28)
3431        offset_sat = LastWeekOfMonth(n=1, weekday=5)
3432
3433        one_day_before = last_sat + timedelta(days=-1)
3434        assert one_day_before + offset_sat == last_sat
3435
3436        one_day_after = last_sat + timedelta(days=+1)
3437        assert one_day_after + offset_sat == next_sat
3438
3439        # Test On that day
3440        assert last_sat + offset_sat == next_sat
3441
3442        # Thursday
3443
3444        offset_thur = LastWeekOfMonth(n=1, weekday=3)
3445        last_thurs = datetime(2013, 1, 31)
3446        next_thurs = datetime(2013, 2, 28)
3447
3448        one_day_before = last_thurs + timedelta(days=-1)
3449        assert one_day_before + offset_thur == last_thurs
3450
3451        one_day_after = last_thurs + timedelta(days=+1)
3452        assert one_day_after + offset_thur == next_thurs
3453
3454        # Test on that day
3455        assert last_thurs + offset_thur == next_thurs
3456
3457        three_before = last_thurs + timedelta(days=-3)
3458        assert three_before + offset_thur == last_thurs
3459
3460        two_after = last_thurs + timedelta(days=+2)
3461        assert two_after + offset_thur == next_thurs
3462
3463        offset_sunday = LastWeekOfMonth(n=1, weekday=WeekDay.SUN)
3464        assert datetime(2013, 7, 31) + offset_sunday == datetime(2013, 8, 25)
3465
3466    on_offset_cases = [
3467        (WeekDay.SUN, datetime(2013, 1, 27), True),
3468        (WeekDay.SAT, datetime(2013, 3, 30), True),
3469        (WeekDay.MON, datetime(2013, 2, 18), False),  # Not the last Mon
3470        (WeekDay.SUN, datetime(2013, 2, 25), False),  # Not a SUN
3471        (WeekDay.MON, datetime(2013, 2, 25), True),
3472        (WeekDay.SAT, datetime(2013, 11, 30), True),
3473        (WeekDay.SAT, datetime(2006, 8, 26), True),
3474        (WeekDay.SAT, datetime(2007, 8, 25), True),
3475        (WeekDay.SAT, datetime(2008, 8, 30), True),
3476        (WeekDay.SAT, datetime(2009, 8, 29), True),
3477        (WeekDay.SAT, datetime(2010, 8, 28), True),
3478        (WeekDay.SAT, datetime(2011, 8, 27), True),
3479        (WeekDay.SAT, datetime(2019, 8, 31), True),
3480    ]
3481
3482    @pytest.mark.parametrize("case", on_offset_cases)
3483    def test_is_on_offset(self, case):
3484        weekday, dt, expected = case
3485        offset = LastWeekOfMonth(weekday=weekday)
3486        assert offset.is_on_offset(dt) == expected
3487
3488    def test_repr(self):
3489        assert (
3490            repr(LastWeekOfMonth(n=2, weekday=1)) == "<2 * LastWeekOfMonths: weekday=1>"
3491        )
3492
3493
3494class TestSemiMonthEnd(Base):
3495    _offset = SemiMonthEnd
3496    offset1 = _offset()
3497    offset2 = _offset(2)
3498
3499    def test_offset_whole_year(self):
3500        dates = (
3501            datetime(2007, 12, 31),
3502            datetime(2008, 1, 15),
3503            datetime(2008, 1, 31),
3504            datetime(2008, 2, 15),
3505            datetime(2008, 2, 29),
3506            datetime(2008, 3, 15),
3507            datetime(2008, 3, 31),
3508            datetime(2008, 4, 15),
3509            datetime(2008, 4, 30),
3510            datetime(2008, 5, 15),
3511            datetime(2008, 5, 31),
3512            datetime(2008, 6, 15),
3513            datetime(2008, 6, 30),
3514            datetime(2008, 7, 15),
3515            datetime(2008, 7, 31),
3516            datetime(2008, 8, 15),
3517            datetime(2008, 8, 31),
3518            datetime(2008, 9, 15),
3519            datetime(2008, 9, 30),
3520            datetime(2008, 10, 15),
3521            datetime(2008, 10, 31),
3522            datetime(2008, 11, 15),
3523            datetime(2008, 11, 30),
3524            datetime(2008, 12, 15),
3525            datetime(2008, 12, 31),
3526        )
3527
3528        for base, exp_date in zip(dates[:-1], dates[1:]):
3529            assert_offset_equal(SemiMonthEnd(), base, exp_date)
3530
3531        # ensure .apply_index works as expected
3532        s = DatetimeIndex(dates[:-1])
3533        with tm.assert_produces_warning(None):
3534            # GH#22535 check that we don't get a FutureWarning from adding
3535            # an integer array to PeriodIndex
3536            result = SemiMonthEnd() + s
3537
3538        exp = DatetimeIndex(dates[1:])
3539        tm.assert_index_equal(result, exp)
3540
3541        # ensure generating a range with DatetimeIndex gives same result
3542        result = date_range(start=dates[0], end=dates[-1], freq="SM")
3543        exp = DatetimeIndex(dates, freq="SM")
3544        tm.assert_index_equal(result, exp)
3545
3546    offset_cases = []
3547    offset_cases.append(
3548        (
3549            SemiMonthEnd(),
3550            {
3551                datetime(2008, 1, 1): datetime(2008, 1, 15),
3552                datetime(2008, 1, 15): datetime(2008, 1, 31),
3553                datetime(2008, 1, 31): datetime(2008, 2, 15),
3554                datetime(2006, 12, 14): datetime(2006, 12, 15),
3555                datetime(2006, 12, 29): datetime(2006, 12, 31),
3556                datetime(2006, 12, 31): datetime(2007, 1, 15),
3557                datetime(2007, 1, 1): datetime(2007, 1, 15),
3558                datetime(2006, 12, 1): datetime(2006, 12, 15),
3559                datetime(2006, 12, 15): datetime(2006, 12, 31),
3560            },
3561        )
3562    )
3563
3564    offset_cases.append(
3565        (
3566            SemiMonthEnd(day_of_month=20),
3567            {
3568                datetime(2008, 1, 1): datetime(2008, 1, 20),
3569                datetime(2008, 1, 15): datetime(2008, 1, 20),
3570                datetime(2008, 1, 21): datetime(2008, 1, 31),
3571                datetime(2008, 1, 31): datetime(2008, 2, 20),
3572                datetime(2006, 12, 14): datetime(2006, 12, 20),
3573                datetime(2006, 12, 29): datetime(2006, 12, 31),
3574                datetime(2006, 12, 31): datetime(2007, 1, 20),
3575                datetime(2007, 1, 1): datetime(2007, 1, 20),
3576                datetime(2006, 12, 1): datetime(2006, 12, 20),
3577                datetime(2006, 12, 15): datetime(2006, 12, 20),
3578            },
3579        )
3580    )
3581
3582    offset_cases.append(
3583        (
3584            SemiMonthEnd(0),
3585            {
3586                datetime(2008, 1, 1): datetime(2008, 1, 15),
3587                datetime(2008, 1, 16): datetime(2008, 1, 31),
3588                datetime(2008, 1, 15): datetime(2008, 1, 15),
3589                datetime(2008, 1, 31): datetime(2008, 1, 31),
3590                datetime(2006, 12, 29): datetime(2006, 12, 31),
3591                datetime(2006, 12, 31): datetime(2006, 12, 31),
3592                datetime(2007, 1, 1): datetime(2007, 1, 15),
3593            },
3594        )
3595    )
3596
3597    offset_cases.append(
3598        (
3599            SemiMonthEnd(0, day_of_month=16),
3600            {
3601                datetime(2008, 1, 1): datetime(2008, 1, 16),
3602                datetime(2008, 1, 16): datetime(2008, 1, 16),
3603                datetime(2008, 1, 15): datetime(2008, 1, 16),
3604                datetime(2008, 1, 31): datetime(2008, 1, 31),
3605                datetime(2006, 12, 29): datetime(2006, 12, 31),
3606                datetime(2006, 12, 31): datetime(2006, 12, 31),
3607                datetime(2007, 1, 1): datetime(2007, 1, 16),
3608            },
3609        )
3610    )
3611
3612    offset_cases.append(
3613        (
3614            SemiMonthEnd(2),
3615            {
3616                datetime(2008, 1, 1): datetime(2008, 1, 31),
3617                datetime(2008, 1, 31): datetime(2008, 2, 29),
3618                datetime(2006, 12, 29): datetime(2007, 1, 15),
3619                datetime(2006, 12, 31): datetime(2007, 1, 31),
3620                datetime(2007, 1, 1): datetime(2007, 1, 31),
3621                datetime(2007, 1, 16): datetime(2007, 2, 15),
3622                datetime(2006, 11, 1): datetime(2006, 11, 30),
3623            },
3624        )
3625    )
3626
3627    offset_cases.append(
3628        (
3629            SemiMonthEnd(-1),
3630            {
3631                datetime(2007, 1, 1): datetime(2006, 12, 31),
3632                datetime(2008, 6, 30): datetime(2008, 6, 15),
3633                datetime(2008, 12, 31): datetime(2008, 12, 15),
3634                datetime(2006, 12, 29): datetime(2006, 12, 15),
3635                datetime(2006, 12, 30): datetime(2006, 12, 15),
3636                datetime(2007, 1, 1): datetime(2006, 12, 31),
3637            },
3638        )
3639    )
3640
3641    offset_cases.append(
3642        (
3643            SemiMonthEnd(-1, day_of_month=4),
3644            {
3645                datetime(2007, 1, 1): datetime(2006, 12, 31),
3646                datetime(2007, 1, 4): datetime(2006, 12, 31),
3647                datetime(2008, 6, 30): datetime(2008, 6, 4),
3648                datetime(2008, 12, 31): datetime(2008, 12, 4),
3649                datetime(2006, 12, 5): datetime(2006, 12, 4),
3650                datetime(2006, 12, 30): datetime(2006, 12, 4),
3651                datetime(2007, 1, 1): datetime(2006, 12, 31),
3652            },
3653        )
3654    )
3655
3656    offset_cases.append(
3657        (
3658            SemiMonthEnd(-2),
3659            {
3660                datetime(2007, 1, 1): datetime(2006, 12, 15),
3661                datetime(2008, 6, 30): datetime(2008, 5, 31),
3662                datetime(2008, 3, 15): datetime(2008, 2, 15),
3663                datetime(2008, 12, 31): datetime(2008, 11, 30),
3664                datetime(2006, 12, 29): datetime(2006, 11, 30),
3665                datetime(2006, 12, 14): datetime(2006, 11, 15),
3666                datetime(2007, 1, 1): datetime(2006, 12, 15),
3667            },
3668        )
3669    )
3670
3671    @pytest.mark.parametrize("case", offset_cases)
3672    def test_offset(self, case):
3673        offset, cases = case
3674        for base, expected in cases.items():
3675            assert_offset_equal(offset, base, expected)
3676
3677    @pytest.mark.parametrize("case", offset_cases)
3678    def test_apply_index(self, case):
3679        # https://github.com/pandas-dev/pandas/issues/34580
3680        offset, cases = case
3681        s = DatetimeIndex(cases.keys())
3682        exp = DatetimeIndex(cases.values())
3683
3684        with tm.assert_produces_warning(None):
3685            # GH#22535 check that we don't get a FutureWarning from adding
3686            # an integer array to PeriodIndex
3687            result = offset + s
3688        tm.assert_index_equal(result, exp)
3689
3690        with tm.assert_produces_warning(FutureWarning):
3691            result = offset.apply_index(s)
3692        tm.assert_index_equal(result, exp)
3693
3694    on_offset_cases = [
3695        (datetime(2007, 12, 31), True),
3696        (datetime(2007, 12, 15), True),
3697        (datetime(2007, 12, 14), False),
3698        (datetime(2007, 12, 1), False),
3699        (datetime(2008, 2, 29), True),
3700    ]
3701
3702    @pytest.mark.parametrize("case", on_offset_cases)
3703    def test_is_on_offset(self, case):
3704        dt, expected = case
3705        assert_is_on_offset(SemiMonthEnd(), dt, expected)
3706
3707    @pytest.mark.parametrize("klass", [Series, DatetimeIndex])
3708    def test_vectorized_offset_addition(self, klass):
3709        s = klass(
3710            [
3711                Timestamp("2000-01-15 00:15:00", tz="US/Central"),
3712                Timestamp("2000-02-15", tz="US/Central"),
3713            ],
3714            name="a",
3715        )
3716
3717        with tm.assert_produces_warning(None):
3718            # GH#22535 check that we don't get a FutureWarning from adding
3719            # an integer array to PeriodIndex
3720            result = s + SemiMonthEnd()
3721            result2 = SemiMonthEnd() + s
3722
3723        exp = klass(
3724            [
3725                Timestamp("2000-01-31 00:15:00", tz="US/Central"),
3726                Timestamp("2000-02-29", tz="US/Central"),
3727            ],
3728            name="a",
3729        )
3730        tm.assert_equal(result, exp)
3731        tm.assert_equal(result2, exp)
3732
3733        s = klass(
3734            [
3735                Timestamp("2000-01-01 00:15:00", tz="US/Central"),
3736                Timestamp("2000-02-01", tz="US/Central"),
3737            ],
3738            name="a",
3739        )
3740
3741        with tm.assert_produces_warning(None):
3742            # GH#22535 check that we don't get a FutureWarning from adding
3743            # an integer array to PeriodIndex
3744            result = s + SemiMonthEnd()
3745            result2 = SemiMonthEnd() + s
3746
3747        exp = klass(
3748            [
3749                Timestamp("2000-01-15 00:15:00", tz="US/Central"),
3750                Timestamp("2000-02-15", tz="US/Central"),
3751            ],
3752            name="a",
3753        )
3754        tm.assert_equal(result, exp)
3755        tm.assert_equal(result2, exp)
3756
3757
3758class TestSemiMonthBegin(Base):
3759    _offset = SemiMonthBegin
3760    offset1 = _offset()
3761    offset2 = _offset(2)
3762
3763    def test_offset_whole_year(self):
3764        dates = (
3765            datetime(2007, 12, 15),
3766            datetime(2008, 1, 1),
3767            datetime(2008, 1, 15),
3768            datetime(2008, 2, 1),
3769            datetime(2008, 2, 15),
3770            datetime(2008, 3, 1),
3771            datetime(2008, 3, 15),
3772            datetime(2008, 4, 1),
3773            datetime(2008, 4, 15),
3774            datetime(2008, 5, 1),
3775            datetime(2008, 5, 15),
3776            datetime(2008, 6, 1),
3777            datetime(2008, 6, 15),
3778            datetime(2008, 7, 1),
3779            datetime(2008, 7, 15),
3780            datetime(2008, 8, 1),
3781            datetime(2008, 8, 15),
3782            datetime(2008, 9, 1),
3783            datetime(2008, 9, 15),
3784            datetime(2008, 10, 1),
3785            datetime(2008, 10, 15),
3786            datetime(2008, 11, 1),
3787            datetime(2008, 11, 15),
3788            datetime(2008, 12, 1),
3789            datetime(2008, 12, 15),
3790        )
3791
3792        for base, exp_date in zip(dates[:-1], dates[1:]):
3793            assert_offset_equal(SemiMonthBegin(), base, exp_date)
3794
3795        # ensure .apply_index works as expected
3796        s = DatetimeIndex(dates[:-1])
3797        with tm.assert_produces_warning(None):
3798            # GH#22535 check that we don't get a FutureWarning from adding
3799            # an integer array to PeriodIndex
3800            result = SemiMonthBegin() + s
3801
3802        exp = DatetimeIndex(dates[1:])
3803        tm.assert_index_equal(result, exp)
3804
3805        # ensure generating a range with DatetimeIndex gives same result
3806        result = date_range(start=dates[0], end=dates[-1], freq="SMS")
3807        exp = DatetimeIndex(dates, freq="SMS")
3808        tm.assert_index_equal(result, exp)
3809
3810    offset_cases = []
3811    offset_cases.append(
3812        (
3813            SemiMonthBegin(),
3814            {
3815                datetime(2008, 1, 1): datetime(2008, 1, 15),
3816                datetime(2008, 1, 15): datetime(2008, 2, 1),
3817                datetime(2008, 1, 31): datetime(2008, 2, 1),
3818                datetime(2006, 12, 14): datetime(2006, 12, 15),
3819                datetime(2006, 12, 29): datetime(2007, 1, 1),
3820                datetime(2006, 12, 31): datetime(2007, 1, 1),
3821                datetime(2007, 1, 1): datetime(2007, 1, 15),
3822                datetime(2006, 12, 1): datetime(2006, 12, 15),
3823                datetime(2006, 12, 15): datetime(2007, 1, 1),
3824            },
3825        )
3826    )
3827
3828    offset_cases.append(
3829        (
3830            SemiMonthBegin(day_of_month=20),
3831            {
3832                datetime(2008, 1, 1): datetime(2008, 1, 20),
3833                datetime(2008, 1, 15): datetime(2008, 1, 20),
3834                datetime(2008, 1, 21): datetime(2008, 2, 1),
3835                datetime(2008, 1, 31): datetime(2008, 2, 1),
3836                datetime(2006, 12, 14): datetime(2006, 12, 20),
3837                datetime(2006, 12, 29): datetime(2007, 1, 1),
3838                datetime(2006, 12, 31): datetime(2007, 1, 1),
3839                datetime(2007, 1, 1): datetime(2007, 1, 20),
3840                datetime(2006, 12, 1): datetime(2006, 12, 20),
3841                datetime(2006, 12, 15): datetime(2006, 12, 20),
3842            },
3843        )
3844    )
3845
3846    offset_cases.append(
3847        (
3848            SemiMonthBegin(0),
3849            {
3850                datetime(2008, 1, 1): datetime(2008, 1, 1),
3851                datetime(2008, 1, 16): datetime(2008, 2, 1),
3852                datetime(2008, 1, 15): datetime(2008, 1, 15),
3853                datetime(2008, 1, 31): datetime(2008, 2, 1),
3854                datetime(2006, 12, 29): datetime(2007, 1, 1),
3855                datetime(2006, 12, 2): datetime(2006, 12, 15),
3856                datetime(2007, 1, 1): datetime(2007, 1, 1),
3857            },
3858        )
3859    )
3860
3861    offset_cases.append(
3862        (
3863            SemiMonthBegin(0, day_of_month=16),
3864            {
3865                datetime(2008, 1, 1): datetime(2008, 1, 1),
3866                datetime(2008, 1, 16): datetime(2008, 1, 16),
3867                datetime(2008, 1, 15): datetime(2008, 1, 16),
3868                datetime(2008, 1, 31): datetime(2008, 2, 1),
3869                datetime(2006, 12, 29): datetime(2007, 1, 1),
3870                datetime(2006, 12, 31): datetime(2007, 1, 1),
3871                datetime(2007, 1, 5): datetime(2007, 1, 16),
3872                datetime(2007, 1, 1): datetime(2007, 1, 1),
3873            },
3874        )
3875    )
3876
3877    offset_cases.append(
3878        (
3879            SemiMonthBegin(2),
3880            {
3881                datetime(2008, 1, 1): datetime(2008, 2, 1),
3882                datetime(2008, 1, 31): datetime(2008, 2, 15),
3883                datetime(2006, 12, 1): datetime(2007, 1, 1),
3884                datetime(2006, 12, 29): datetime(2007, 1, 15),
3885                datetime(2006, 12, 15): datetime(2007, 1, 15),
3886                datetime(2007, 1, 1): datetime(2007, 2, 1),
3887                datetime(2007, 1, 16): datetime(2007, 2, 15),
3888                datetime(2006, 11, 1): datetime(2006, 12, 1),
3889            },
3890        )
3891    )
3892
3893    offset_cases.append(
3894        (
3895            SemiMonthBegin(-1),
3896            {
3897                datetime(2007, 1, 1): datetime(2006, 12, 15),
3898                datetime(2008, 6, 30): datetime(2008, 6, 15),
3899                datetime(2008, 6, 14): datetime(2008, 6, 1),
3900                datetime(2008, 12, 31): datetime(2008, 12, 15),
3901                datetime(2006, 12, 29): datetime(2006, 12, 15),
3902                datetime(2006, 12, 15): datetime(2006, 12, 1),
3903                datetime(2007, 1, 1): datetime(2006, 12, 15),
3904            },
3905        )
3906    )
3907
3908    offset_cases.append(
3909        (
3910            SemiMonthBegin(-1, day_of_month=4),
3911            {
3912                datetime(2007, 1, 1): datetime(2006, 12, 4),
3913                datetime(2007, 1, 4): datetime(2007, 1, 1),
3914                datetime(2008, 6, 30): datetime(2008, 6, 4),
3915                datetime(2008, 12, 31): datetime(2008, 12, 4),
3916                datetime(2006, 12, 5): datetime(2006, 12, 4),
3917                datetime(2006, 12, 30): datetime(2006, 12, 4),
3918                datetime(2006, 12, 2): datetime(2006, 12, 1),
3919                datetime(2007, 1, 1): datetime(2006, 12, 4),
3920            },
3921        )
3922    )
3923
3924    offset_cases.append(
3925        (
3926            SemiMonthBegin(-2),
3927            {
3928                datetime(2007, 1, 1): datetime(2006, 12, 1),
3929                datetime(2008, 6, 30): datetime(2008, 6, 1),
3930                datetime(2008, 6, 14): datetime(2008, 5, 15),
3931                datetime(2008, 12, 31): datetime(2008, 12, 1),
3932                datetime(2006, 12, 29): datetime(2006, 12, 1),
3933                datetime(2006, 12, 15): datetime(2006, 11, 15),
3934                datetime(2007, 1, 1): datetime(2006, 12, 1),
3935            },
3936        )
3937    )
3938
3939    @pytest.mark.parametrize("case", offset_cases)
3940    def test_offset(self, case):
3941        offset, cases = case
3942        for base, expected in cases.items():
3943            assert_offset_equal(offset, base, expected)
3944
3945    @pytest.mark.parametrize("case", offset_cases)
3946    def test_apply_index(self, case):
3947        offset, cases = case
3948        s = DatetimeIndex(cases.keys())
3949
3950        with tm.assert_produces_warning(None):
3951            # GH#22535 check that we don't get a FutureWarning from adding
3952            # an integer array to PeriodIndex
3953            result = offset + s
3954
3955        exp = DatetimeIndex(cases.values())
3956        tm.assert_index_equal(result, exp)
3957
3958    on_offset_cases = [
3959        (datetime(2007, 12, 1), True),
3960        (datetime(2007, 12, 15), True),
3961        (datetime(2007, 12, 14), False),
3962        (datetime(2007, 12, 31), False),
3963        (datetime(2008, 2, 15), True),
3964    ]
3965
3966    @pytest.mark.parametrize("case", on_offset_cases)
3967    def test_is_on_offset(self, case):
3968        dt, expected = case
3969        assert_is_on_offset(SemiMonthBegin(), dt, expected)
3970
3971    @pytest.mark.parametrize("klass", [Series, DatetimeIndex])
3972    def test_vectorized_offset_addition(self, klass):
3973        s = klass(
3974            [
3975                Timestamp("2000-01-15 00:15:00", tz="US/Central"),
3976                Timestamp("2000-02-15", tz="US/Central"),
3977            ],
3978            name="a",
3979        )
3980        with tm.assert_produces_warning(None):
3981            # GH#22535 check that we don't get a FutureWarning from adding
3982            # an integer array to PeriodIndex
3983            result = s + SemiMonthBegin()
3984            result2 = SemiMonthBegin() + s
3985
3986        exp = klass(
3987            [
3988                Timestamp("2000-02-01 00:15:00", tz="US/Central"),
3989                Timestamp("2000-03-01", tz="US/Central"),
3990            ],
3991            name="a",
3992        )
3993        tm.assert_equal(result, exp)
3994        tm.assert_equal(result2, exp)
3995
3996        s = klass(
3997            [
3998                Timestamp("2000-01-01 00:15:00", tz="US/Central"),
3999                Timestamp("2000-02-01", tz="US/Central"),
4000            ],
4001            name="a",
4002        )
4003        with tm.assert_produces_warning(None):
4004            # GH#22535 check that we don't get a FutureWarning from adding
4005            # an integer array to PeriodIndex
4006            result = s + SemiMonthBegin()
4007            result2 = SemiMonthBegin() + s
4008
4009        exp = klass(
4010            [
4011                Timestamp("2000-01-15 00:15:00", tz="US/Central"),
4012                Timestamp("2000-02-15", tz="US/Central"),
4013            ],
4014            name="a",
4015        )
4016        tm.assert_equal(result, exp)
4017        tm.assert_equal(result2, exp)
4018
4019
4020def test_Easter():
4021    assert_offset_equal(Easter(), datetime(2010, 1, 1), datetime(2010, 4, 4))
4022    assert_offset_equal(Easter(), datetime(2010, 4, 5), datetime(2011, 4, 24))
4023    assert_offset_equal(Easter(2), datetime(2010, 1, 1), datetime(2011, 4, 24))
4024
4025    assert_offset_equal(Easter(), datetime(2010, 4, 4), datetime(2011, 4, 24))
4026    assert_offset_equal(Easter(2), datetime(2010, 4, 4), datetime(2012, 4, 8))
4027
4028    assert_offset_equal(-Easter(), datetime(2011, 1, 1), datetime(2010, 4, 4))
4029    assert_offset_equal(-Easter(), datetime(2010, 4, 5), datetime(2010, 4, 4))
4030    assert_offset_equal(-Easter(2), datetime(2011, 1, 1), datetime(2009, 4, 12))
4031
4032    assert_offset_equal(-Easter(), datetime(2010, 4, 4), datetime(2009, 4, 12))
4033    assert_offset_equal(-Easter(2), datetime(2010, 4, 4), datetime(2008, 3, 23))
4034
4035
4036class TestOffsetNames:
4037    def test_get_offset_name(self):
4038        assert BDay().freqstr == "B"
4039        assert BDay(2).freqstr == "2B"
4040        assert BMonthEnd().freqstr == "BM"
4041        assert Week(weekday=0).freqstr == "W-MON"
4042        assert Week(weekday=1).freqstr == "W-TUE"
4043        assert Week(weekday=2).freqstr == "W-WED"
4044        assert Week(weekday=3).freqstr == "W-THU"
4045        assert Week(weekday=4).freqstr == "W-FRI"
4046
4047        assert LastWeekOfMonth(weekday=WeekDay.SUN).freqstr == "LWOM-SUN"
4048
4049
4050def test_get_offset():
4051    with pytest.raises(ValueError, match=INVALID_FREQ_ERR_MSG):
4052        _get_offset("gibberish")
4053    with pytest.raises(ValueError, match=INVALID_FREQ_ERR_MSG):
4054        _get_offset("QS-JAN-B")
4055
4056    pairs = [
4057        ("B", BDay()),
4058        ("b", BDay()),
4059        ("bm", BMonthEnd()),
4060        ("Bm", BMonthEnd()),
4061        ("W-MON", Week(weekday=0)),
4062        ("W-TUE", Week(weekday=1)),
4063        ("W-WED", Week(weekday=2)),
4064        ("W-THU", Week(weekday=3)),
4065        ("W-FRI", Week(weekday=4)),
4066    ]
4067
4068    for name, expected in pairs:
4069        offset = _get_offset(name)
4070        assert offset == expected, (
4071            f"Expected {repr(name)} to yield {repr(expected)} "
4072            f"(actual: {repr(offset)})"
4073        )
4074
4075
4076def test_get_offset_legacy():
4077    pairs = [("w@Sat", Week(weekday=5))]
4078    for name, expected in pairs:
4079        with pytest.raises(ValueError, match=INVALID_FREQ_ERR_MSG):
4080            _get_offset(name)
4081
4082
4083class TestOffsetAliases:
4084    def setup_method(self, method):
4085        _offset_map.clear()
4086
4087    def test_alias_equality(self):
4088        for k, v in _offset_map.items():
4089            if v is None:
4090                continue
4091            assert k == v.copy()
4092
4093    def test_rule_code(self):
4094        lst = ["M", "MS", "BM", "BMS", "D", "B", "H", "T", "S", "L", "U"]
4095        for k in lst:
4096            assert k == _get_offset(k).rule_code
4097            # should be cached - this is kind of an internals test...
4098            assert k in _offset_map
4099            assert k == (_get_offset(k) * 3).rule_code
4100
4101        suffix_lst = ["MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"]
4102        base = "W"
4103        for v in suffix_lst:
4104            alias = "-".join([base, v])
4105            assert alias == _get_offset(alias).rule_code
4106            assert alias == (_get_offset(alias) * 5).rule_code
4107
4108        suffix_lst = [
4109            "JAN",
4110            "FEB",
4111            "MAR",
4112            "APR",
4113            "MAY",
4114            "JUN",
4115            "JUL",
4116            "AUG",
4117            "SEP",
4118            "OCT",
4119            "NOV",
4120            "DEC",
4121        ]
4122        base_lst = ["A", "AS", "BA", "BAS", "Q", "QS", "BQ", "BQS"]
4123        for base in base_lst:
4124            for v in suffix_lst:
4125                alias = "-".join([base, v])
4126                assert alias == _get_offset(alias).rule_code
4127                assert alias == (_get_offset(alias) * 5).rule_code
4128
4129
4130def test_dateoffset_misc():
4131    oset = offsets.DateOffset(months=2, days=4)
4132    # it works
4133    oset.freqstr
4134
4135    assert not offsets.DateOffset(months=2) == 2
4136
4137
4138def test_freq_offsets():
4139    off = BDay(1, offset=timedelta(0, 1800))
4140    assert off.freqstr == "B+30Min"
4141
4142    off = BDay(1, offset=timedelta(0, -1800))
4143    assert off.freqstr == "B-30Min"
4144
4145
4146class TestReprNames:
4147    def test_str_for_named_is_name(self):
4148        # look at all the amazing combinations!
4149        month_prefixes = ["A", "AS", "BA", "BAS", "Q", "BQ", "BQS", "QS"]
4150        names = [
4151            prefix + "-" + month
4152            for prefix in month_prefixes
4153            for month in [
4154                "JAN",
4155                "FEB",
4156                "MAR",
4157                "APR",
4158                "MAY",
4159                "JUN",
4160                "JUL",
4161                "AUG",
4162                "SEP",
4163                "OCT",
4164                "NOV",
4165                "DEC",
4166            ]
4167        ]
4168        days = ["MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"]
4169        names += ["W-" + day for day in days]
4170        names += ["WOM-" + week + day for week in ("1", "2", "3", "4") for day in days]
4171        _offset_map.clear()
4172        for name in names:
4173            offset = _get_offset(name)
4174            assert offset.freqstr == name
4175
4176
4177def get_utc_offset_hours(ts):
4178    # take a Timestamp and compute total hours of utc offset
4179    o = ts.utcoffset()
4180    return (o.days * 24 * 3600 + o.seconds) / 3600.0
4181
4182
4183class TestDST:
4184    """
4185    test DateOffset additions over Daylight Savings Time
4186    """
4187
4188    # one microsecond before the DST transition
4189    ts_pre_fallback = "2013-11-03 01:59:59.999999"
4190    ts_pre_springfwd = "2013-03-10 01:59:59.999999"
4191
4192    # test both basic names and dateutil timezones
4193    timezone_utc_offsets = {
4194        "US/Eastern": {"utc_offset_daylight": -4, "utc_offset_standard": -5},
4195        "dateutil/US/Pacific": {"utc_offset_daylight": -7, "utc_offset_standard": -8},
4196    }
4197    valid_date_offsets_singular = [
4198        "weekday",
4199        "day",
4200        "hour",
4201        "minute",
4202        "second",
4203        "microsecond",
4204    ]
4205    valid_date_offsets_plural = [
4206        "weeks",
4207        "days",
4208        "hours",
4209        "minutes",
4210        "seconds",
4211        "milliseconds",
4212        "microseconds",
4213    ]
4214
4215    def _test_all_offsets(self, n, **kwds):
4216        valid_offsets = (
4217            self.valid_date_offsets_plural
4218            if n > 1
4219            else self.valid_date_offsets_singular
4220        )
4221
4222        for name in valid_offsets:
4223            self._test_offset(offset_name=name, offset_n=n, **kwds)
4224
4225    def _test_offset(self, offset_name, offset_n, tstart, expected_utc_offset):
4226        offset = DateOffset(**{offset_name: offset_n})
4227
4228        t = tstart + offset
4229        if expected_utc_offset is not None:
4230            assert get_utc_offset_hours(t) == expected_utc_offset
4231
4232        if offset_name == "weeks":
4233            # dates should match
4234            assert t.date() == timedelta(days=7 * offset.kwds["weeks"]) + tstart.date()
4235            # expect the same day of week, hour of day, minute, second, ...
4236            assert (
4237                t.dayofweek == tstart.dayofweek
4238                and t.hour == tstart.hour
4239                and t.minute == tstart.minute
4240                and t.second == tstart.second
4241            )
4242        elif offset_name == "days":
4243            # dates should match
4244            assert timedelta(offset.kwds["days"]) + tstart.date() == t.date()
4245            # expect the same hour of day, minute, second, ...
4246            assert (
4247                t.hour == tstart.hour
4248                and t.minute == tstart.minute
4249                and t.second == tstart.second
4250            )
4251        elif offset_name in self.valid_date_offsets_singular:
4252            # expect the singular offset value to match between tstart and t
4253            datepart_offset = getattr(
4254                t, offset_name if offset_name != "weekday" else "dayofweek"
4255            )
4256            assert datepart_offset == offset.kwds[offset_name]
4257        else:
4258            # the offset should be the same as if it was done in UTC
4259            assert t == (tstart.tz_convert("UTC") + offset).tz_convert("US/Pacific")
4260
4261    def _make_timestamp(self, string, hrs_offset, tz):
4262        if hrs_offset >= 0:
4263            offset_string = f"{hrs_offset:02d}00"
4264        else:
4265            offset_string = f"-{(hrs_offset * -1):02}00"
4266        return Timestamp(string + offset_string).tz_convert(tz)
4267
4268    def test_springforward_plural(self):
4269        # test moving from standard to daylight savings
4270        for tz, utc_offsets in self.timezone_utc_offsets.items():
4271            hrs_pre = utc_offsets["utc_offset_standard"]
4272            hrs_post = utc_offsets["utc_offset_daylight"]
4273            self._test_all_offsets(
4274                n=3,
4275                tstart=self._make_timestamp(self.ts_pre_springfwd, hrs_pre, tz),
4276                expected_utc_offset=hrs_post,
4277            )
4278
4279    def test_fallback_singular(self):
4280        # in the case of singular offsets, we don't necessarily know which utc
4281        # offset the new Timestamp will wind up in (the tz for 1 month may be
4282        # different from 1 second) so we don't specify an expected_utc_offset
4283        for tz, utc_offsets in self.timezone_utc_offsets.items():
4284            hrs_pre = utc_offsets["utc_offset_standard"]
4285            self._test_all_offsets(
4286                n=1,
4287                tstart=self._make_timestamp(self.ts_pre_fallback, hrs_pre, tz),
4288                expected_utc_offset=None,
4289            )
4290
4291    def test_springforward_singular(self):
4292        for tz, utc_offsets in self.timezone_utc_offsets.items():
4293            hrs_pre = utc_offsets["utc_offset_standard"]
4294            self._test_all_offsets(
4295                n=1,
4296                tstart=self._make_timestamp(self.ts_pre_springfwd, hrs_pre, tz),
4297                expected_utc_offset=None,
4298            )
4299
4300    offset_classes = {
4301        MonthBegin: ["11/2/2012", "12/1/2012"],
4302        MonthEnd: ["11/2/2012", "11/30/2012"],
4303        BMonthBegin: ["11/2/2012", "12/3/2012"],
4304        BMonthEnd: ["11/2/2012", "11/30/2012"],
4305        CBMonthBegin: ["11/2/2012", "12/3/2012"],
4306        CBMonthEnd: ["11/2/2012", "11/30/2012"],
4307        SemiMonthBegin: ["11/2/2012", "11/15/2012"],
4308        SemiMonthEnd: ["11/2/2012", "11/15/2012"],
4309        Week: ["11/2/2012", "11/9/2012"],
4310        YearBegin: ["11/2/2012", "1/1/2013"],
4311        YearEnd: ["11/2/2012", "12/31/2012"],
4312        BYearBegin: ["11/2/2012", "1/1/2013"],
4313        BYearEnd: ["11/2/2012", "12/31/2012"],
4314        QuarterBegin: ["11/2/2012", "12/1/2012"],
4315        QuarterEnd: ["11/2/2012", "12/31/2012"],
4316        BQuarterBegin: ["11/2/2012", "12/3/2012"],
4317        BQuarterEnd: ["11/2/2012", "12/31/2012"],
4318        Day: ["11/4/2012", "11/4/2012 23:00"],
4319    }.items()
4320
4321    @pytest.mark.parametrize("tup", offset_classes)
4322    def test_all_offset_classes(self, tup):
4323        offset, test_values = tup
4324
4325        first = Timestamp(test_values[0], tz="US/Eastern") + offset()
4326        second = Timestamp(test_values[1], tz="US/Eastern")
4327        assert first == second
4328
4329
4330# ---------------------------------------------------------------------
4331
4332
4333def test_valid_default_arguments(offset_types):
4334    # GH#19142 check that the calling the constructors without passing
4335    # any keyword arguments produce valid offsets
4336    cls = offset_types
4337    cls()
4338
4339
4340@pytest.mark.parametrize("kwd", sorted(liboffsets._relativedelta_kwds))
4341def test_valid_month_attributes(kwd, month_classes):
4342    # GH#18226
4343    cls = month_classes
4344    # check that we cannot create e.g. MonthEnd(weeks=3)
4345    msg = rf"__init__\(\) got an unexpected keyword argument '{kwd}'"
4346    with pytest.raises(TypeError, match=msg):
4347        cls(**{kwd: 3})
4348
4349
4350def test_month_offset_name(month_classes):
4351    # GH#33757 off.name with n != 1 should not raise AttributeError
4352    obj = month_classes(1)
4353    obj2 = month_classes(2)
4354    assert obj2.name == obj.name
4355
4356
4357@pytest.mark.parametrize("kwd", sorted(liboffsets._relativedelta_kwds))
4358def test_valid_relativedelta_kwargs(kwd):
4359    # Check that all the arguments specified in liboffsets._relativedelta_kwds
4360    # are in fact valid relativedelta keyword args
4361    DateOffset(**{kwd: 1})
4362
4363
4364@pytest.mark.parametrize("kwd", sorted(liboffsets._relativedelta_kwds))
4365def test_valid_tick_attributes(kwd, tick_classes):
4366    # GH#18226
4367    cls = tick_classes
4368    # check that we cannot create e.g. Hour(weeks=3)
4369    msg = rf"__init__\(\) got an unexpected keyword argument '{kwd}'"
4370    with pytest.raises(TypeError, match=msg):
4371        cls(**{kwd: 3})
4372
4373
4374def test_validate_n_error():
4375    with pytest.raises(TypeError, match="argument must be an integer"):
4376        DateOffset(n="Doh!")
4377
4378    with pytest.raises(TypeError, match="argument must be an integer"):
4379        MonthBegin(n=timedelta(1))
4380
4381    with pytest.raises(TypeError, match="argument must be an integer"):
4382        BDay(n=np.array([1, 2], dtype=np.int64))
4383
4384
4385def test_require_integers(offset_types):
4386    cls = offset_types
4387    with pytest.raises(ValueError, match="argument must be an integer"):
4388        cls(n=1.5)
4389
4390
4391def test_tick_normalize_raises(tick_classes):
4392    # check that trying to create a Tick object with normalize=True raises
4393    # GH#21427
4394    cls = tick_classes
4395    msg = "Tick offset with `normalize=True` are not allowed."
4396    with pytest.raises(ValueError, match=msg):
4397        cls(n=3, normalize=True)
4398
4399
4400def test_weeks_onoffset():
4401    # GH#18510 Week with weekday = None, normalize = False should always
4402    # be is_on_offset
4403    offset = Week(n=2, weekday=None)
4404    ts = Timestamp("1862-01-13 09:03:34.873477378+0210", tz="Africa/Lusaka")
4405    fast = offset.is_on_offset(ts)
4406    slow = (ts + offset) - offset == ts
4407    assert fast == slow
4408
4409    # negative n
4410    offset = Week(n=2, weekday=None)
4411    ts = Timestamp("1856-10-24 16:18:36.556360110-0717", tz="Pacific/Easter")
4412    fast = offset.is_on_offset(ts)
4413    slow = (ts + offset) - offset == ts
4414    assert fast == slow
4415
4416
4417def test_weekofmonth_onoffset():
4418    # GH#18864
4419    # Make sure that nanoseconds don't trip up is_on_offset (and with it apply)
4420    offset = WeekOfMonth(n=2, week=2, weekday=0)
4421    ts = Timestamp("1916-05-15 01:14:49.583410462+0422", tz="Asia/Qyzylorda")
4422    fast = offset.is_on_offset(ts)
4423    slow = (ts + offset) - offset == ts
4424    assert fast == slow
4425
4426    # negative n
4427    offset = WeekOfMonth(n=-3, week=1, weekday=0)
4428    ts = Timestamp("1980-12-08 03:38:52.878321185+0500", tz="Asia/Oral")
4429    fast = offset.is_on_offset(ts)
4430    slow = (ts + offset) - offset == ts
4431    assert fast == slow
4432
4433
4434def test_last_week_of_month_on_offset():
4435    # GH#19036, GH#18977 _adjust_dst was incorrect for LastWeekOfMonth
4436    offset = LastWeekOfMonth(n=4, weekday=6)
4437    ts = Timestamp("1917-05-27 20:55:27.084284178+0200", tz="Europe/Warsaw")
4438    slow = (ts + offset) - offset == ts
4439    fast = offset.is_on_offset(ts)
4440    assert fast == slow
4441
4442    # negative n
4443    offset = LastWeekOfMonth(n=-4, weekday=5)
4444    ts = Timestamp("2005-08-27 05:01:42.799392561-0500", tz="America/Rainy_River")
4445    slow = (ts + offset) - offset == ts
4446    fast = offset.is_on_offset(ts)
4447    assert fast == slow
4448
4449
4450def test_week_add_invalid():
4451    # Week with weekday should raise TypeError and _not_ AttributeError
4452    #  when adding invalid offset
4453    offset = Week(weekday=1)
4454    other = Day()
4455    with pytest.raises(TypeError, match="Cannot add"):
4456        offset + other
4457
4458
4459@pytest.mark.parametrize(
4460    "attribute",
4461    [
4462        "hours",
4463        "days",
4464        "weeks",
4465        "months",
4466        "years",
4467    ],
4468)
4469def test_dateoffset_immutable(attribute):
4470    offset = DateOffset(**{attribute: 0})
4471    msg = "DateOffset objects are immutable"
4472    with pytest.raises(AttributeError, match=msg):
4473        setattr(offset, attribute, 5)
4474