1"""
2Behavioral based tests for offsets and date_range.
3
4This file is adapted from https://github.com/pandas-dev/pandas/pull/18761 -
5which was more ambitious but less idiomatic in its use of Hypothesis.
6
7You may wish to consult the previous version for inspiration on further
8tests, or when trying to pin down the bugs exposed by the tests below.
9"""
10import warnings
11
12from hypothesis import assume, given, strategies as st
13from hypothesis.errors import Flaky
14from hypothesis.extra.dateutil import timezones as dateutil_timezones
15from hypothesis.extra.pytz import timezones as pytz_timezones
16import pytest
17import pytz
18
19import pandas as pd
20from pandas import Timestamp
21
22from pandas.tseries.offsets import (
23    BMonthBegin,
24    BMonthEnd,
25    BQuarterBegin,
26    BQuarterEnd,
27    BYearBegin,
28    BYearEnd,
29    MonthBegin,
30    MonthEnd,
31    QuarterBegin,
32    QuarterEnd,
33    YearBegin,
34    YearEnd,
35)
36
37# ----------------------------------------------------------------
38# Helpers for generating random data
39
40with warnings.catch_warnings():
41    warnings.simplefilter("ignore")
42    min_dt = Timestamp(1900, 1, 1).to_pydatetime()
43    max_dt = Timestamp(1900, 1, 1).to_pydatetime()
44
45gen_date_range = st.builds(
46    pd.date_range,
47    start=st.datetimes(
48        # TODO: Choose the min/max values more systematically
49        min_value=Timestamp(1900, 1, 1).to_pydatetime(),
50        max_value=Timestamp(2100, 1, 1).to_pydatetime(),
51    ),
52    periods=st.integers(min_value=2, max_value=100),
53    freq=st.sampled_from("Y Q M D H T s ms us ns".split()),
54    tz=st.one_of(st.none(), dateutil_timezones(), pytz_timezones()),
55)
56
57gen_random_datetime = st.datetimes(
58    min_value=min_dt,
59    max_value=max_dt,
60    timezones=st.one_of(st.none(), dateutil_timezones(), pytz_timezones()),
61)
62
63# The strategy for each type is registered in conftest.py, as they don't carry
64# enough runtime information (e.g. type hints) to infer how to build them.
65gen_yqm_offset = st.one_of(
66    *map(
67        st.from_type,
68        [
69            MonthBegin,
70            MonthEnd,
71            BMonthBegin,
72            BMonthEnd,
73            QuarterBegin,
74            QuarterEnd,
75            BQuarterBegin,
76            BQuarterEnd,
77            YearBegin,
78            YearEnd,
79            BYearBegin,
80            BYearEnd,
81        ],
82    )
83)
84
85
86# ----------------------------------------------------------------
87# Offset-specific behaviour tests
88
89
90@pytest.mark.arm_slow
91@given(gen_random_datetime, gen_yqm_offset)
92def test_on_offset_implementations(dt, offset):
93    assume(not offset.normalize)
94    # check that the class-specific implementations of is_on_offset match
95    # the general case definition:
96    #   (dt + offset) - offset == dt
97    try:
98        compare = (dt + offset) - offset
99    except pytz.NonExistentTimeError:
100        # dt + offset does not exist, assume(False) to indicate
101        #  to hypothesis that this is not a valid test case
102        assume(False)
103
104    assert offset.is_on_offset(dt) == (compare == dt)
105
106
107@pytest.mark.xfail(strict=False, raises=Flaky, reason="unreliable test timings")
108@given(gen_yqm_offset)
109def test_shift_across_dst(offset):
110    # GH#18319 check that 1) timezone is correctly normalized and
111    # 2) that hour is not incorrectly changed by this normalization
112    assume(not offset.normalize)
113
114    # Note that dti includes a transition across DST boundary
115    dti = pd.date_range(
116        start="2017-10-30 12:00:00", end="2017-11-06", freq="D", tz="US/Eastern"
117    )
118    assert (dti.hour == 12).all()  # we haven't screwed up yet
119
120    res = dti + offset
121    assert (res.hour == 12).all()
122