Skip to content

Commit e53b3da

Browse files
Zac-HDjbrockmendel
andcommitted
TST: Add Hypothesis tests for ticks, offsets
These tests are derived from GH18761, by jbrockmendel Co-authored-by: jbrockmendel <[email protected]>
1 parent 125d035 commit e53b3da

File tree

2 files changed

+163
-0
lines changed

2 files changed

+163
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Behavioral based tests for offsets and date_range.
4+
5+
This file is adapted from https://github.com/pandas-dev/pandas/pull/18761 -
6+
which was more
7+
8+
"""
9+
import pytest
10+
from hypothesis import given, assume, strategies as st
11+
from hypothesis.extra.pytz import timezones as pytz_timezones
12+
from hypothesis.extra.dateutil import timezones as dateutil_timezones
13+
14+
import pandas as pd
15+
16+
from pandas.tseries.offsets import (
17+
Hour, Minute, Second, Milli, Micro, Nano,
18+
MonthEnd, MonthBegin, BMonthEnd, BMonthBegin,
19+
QuarterEnd, QuarterBegin, BQuarterEnd, BQuarterBegin,
20+
YearEnd, YearBegin, BYearEnd, BYearBegin,
21+
)
22+
23+
24+
tick_classes = [Hour, Minute, Second, Milli, Micro, Nano]
25+
yqm_classes = [MonthBegin, MonthEnd, BMonthBegin, BMonthEnd,
26+
QuarterBegin, QuarterEnd, BQuarterBegin, BQuarterEnd,
27+
YearBegin, YearEnd, BYearBegin, BYearEnd]
28+
29+
# ----------------------------------------------------------------
30+
# Helpers for generating random data
31+
32+
gen_date_range = st.builds(
33+
pd.date_range,
34+
start=st.datetimes(
35+
# TODO: Choose the min/max values more systematically
36+
min_value=pd.Timestamp(1900, 1, 1).to_pydatetime(),
37+
max_value=pd.Timestamp(2100, 1, 1).to_pydatetime()
38+
),
39+
periods=st.integers(min_value=2, max_value=100),
40+
freq=st.sampled_from('Y Q M D H T s ms us ns'.split()),
41+
tz=st.one_of(st.none(), dateutil_timezones(), pytz_timezones()),
42+
)
43+
44+
gen_random_datetime = st.datetimes(
45+
min_value=pd.Timestamp.min.to_pydatetime(),
46+
max_value=pd.Timestamp.max.to_pydatetime(),
47+
timezones=st.one_of(st.none(), dateutil_timezones(), pytz_timezones())
48+
)
49+
50+
# Register the various offset classes so st.from_type can create instances.
51+
# We *could* just append the strategies to a list, but this provides a nice
52+
# demo and enables future tests to use a simple e.g. `from_type(Hour)`.
53+
for cls in tick_classes + [MonthBegin, MonthEnd, BMonthBegin, BMonthEnd]:
54+
st.register_type_strategy(cls, st.builds(
55+
cls,
56+
n=st.integers(-99, 99),
57+
normalize=st.booleans(),
58+
))
59+
60+
for cls in [YearBegin, YearEnd, BYearBegin, BYearEnd]:
61+
st.register_type_strategy(cls, st.builds(
62+
cls,
63+
n=st.integers(-5, 5),
64+
normalize=st.booleans(),
65+
month=st.integers(min_value=1, max_value=12),
66+
))
67+
68+
for cls in [QuarterBegin, QuarterEnd, BQuarterBegin, BQuarterEnd]:
69+
st.register_type_strategy(cls, st.builds(
70+
cls,
71+
n=st.integers(-24, 24),
72+
normalize=st.booleans(),
73+
startingMonth=st.integers(min_value=1, max_value=12)
74+
))
75+
76+
# This strategy can generate any kind of Offset in `tick_classes` or
77+
# `yqm_classes`, with arguments as specified directly above in registration.
78+
gen_yqm_offset = st.one_of([st.from_type(cls) for cls in yqm_classes])
79+
80+
81+
# ----------------------------------------------------------------
82+
# Offset-specific behaviour tests
83+
84+
85+
@pytest.mark.xfail()
86+
@given(gen_random_datetime, gen_yqm_offset)
87+
def test_on_offset_implementations(dt, offset):
88+
assume(not offset.normalize)
89+
# check that the class-specific implementations of onOffset match
90+
# the general case definition:
91+
# (dt + offset) - offset == dt
92+
compare = (dt + offset) - offset
93+
assert offset.onOffset(dt) == (compare == dt)
94+
95+
96+
@pytest.mark.xfail()
97+
@given(gen_yqm_offset, gen_date_range)
98+
def test_apply_index_implementations(offset, rng):
99+
# offset.apply_index(dti)[i] should match dti[i] + offset
100+
assume(offset.n != 0) # TODO: test for that case separately
101+
102+
# rng = pd.date_range(start='1/1/2000', periods=100000, freq='T')
103+
ser = pd.Series(rng)
104+
105+
res = rng + offset
106+
res_v2 = offset.apply_index(rng)
107+
assert (res == res_v2).all()
108+
109+
assert res[0] == rng[0] + offset
110+
assert res[-1] == rng[-1] + offset
111+
res2 = ser + offset
112+
# apply_index is only for indexes, not series, so no res2_v2
113+
assert res2.iloc[0] == ser.iloc[0] + offset
114+
assert res2.iloc[-1] == ser.iloc[-1] + offset
115+
# TODO: Check randomly assorted entries, not just first/last
116+
117+
118+
@pytest.mark.xfail()
119+
@given(gen_yqm_offset)
120+
def test_shift_across_dst(offset):
121+
# GH#18319 check that 1) timezone is correctly normalized and
122+
# 2) that hour is not incorrectly changed by this normalization
123+
# Note that dti includes a transition across DST boundary
124+
dti = pd.date_range(start='2017-10-30 12:00:00', end='2017-11-06',
125+
freq='D', tz='US/Eastern')
126+
assert (dti.hour == 12).all() # we haven't screwed up yet
127+
128+
res = dti + offset
129+
assert (res.hour == 12).all()

pandas/tests/tseries/offsets/test_ticks.py

+34
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import pytest
88
import numpy as np
9+
from hypothesis import given, assume, strategies as st
910

1011
from pandas import Timedelta, Timestamp
1112
from pandas.tseries import offsets
@@ -35,6 +36,39 @@ def test_delta_to_tick():
3536
assert (tick == offsets.Day(3))
3637

3738

39+
@given(cls=st.sampled_from(tick_classes),
40+
n=st.integers(-999, 999),
41+
m=st.integers(-999, 999))
42+
def test_tick_add_sub(cls, n, m):
43+
# For all Tick subclasses and all integers n, m, we should have
44+
# tick(n) + tick(m) == tick(n+m)
45+
# tick(n) - tick(m) == tick(n-m)
46+
left = cls(n)
47+
right = cls(m)
48+
expected = cls(n + m)
49+
50+
assert left + right == expected
51+
assert left.apply(right) == expected
52+
53+
expected = cls(n - m)
54+
assert left - right == expected
55+
56+
57+
@given(cls=st.sampled_from(tick_classes),
58+
n=st.integers(-999, 999), m=st.integers(-999, 999))
59+
def test_tick_equality(cls, n, m):
60+
assume(m != n)
61+
# tick == tock iff tick.n == tock.n
62+
left = cls(n)
63+
right = cls(m)
64+
assert left != right
65+
assert not (left == right)
66+
67+
right = cls(n)
68+
assert left == right
69+
assert not (left != right)
70+
71+
3872
# ---------------------------------------------------------------------
3973

4074

0 commit comments

Comments
 (0)