Skip to content

Commit 5112ea8

Browse files
committed
tests(timestepping): write full tests for the timestepping module
1 parent 1cac6d7 commit 5112ea8

9 files changed

Lines changed: 2752 additions & 62 deletions
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
"""Comprehensive tests for timestep estimation functionality."""
2+
3+
import datetime
4+
import pytest
5+
from d3tools.timestepping import estimate_timestep, Day, Hour, Month, Year, Dekad
6+
from d3tools.timestepping.timeperiods.fixed_doy_timestep import ViirsModisTimeStep
7+
8+
9+
class TestEstimateTimestep:
10+
"""Test suite for estimate_timestep function."""
11+
12+
def test_estimate_daily(self):
13+
"""Test estimating daily timesteps."""
14+
# Daily timesteps
15+
dates = [
16+
datetime.datetime(2024, 1, 1),
17+
datetime.datetime(2024, 1, 2),
18+
datetime.datetime(2024, 1, 3),
19+
datetime.datetime(2024, 1, 4),
20+
datetime.datetime(2024, 1, 5),
21+
]
22+
23+
result = estimate_timestep(dates)
24+
assert result == Day
25+
26+
def test_estimate_hourly(self):
27+
"""Test estimating hourly timesteps."""
28+
# Hourly timesteps
29+
dates = [
30+
datetime.datetime(2024, 1, 1, 0),
31+
datetime.datetime(2024, 1, 1, 1),
32+
datetime.datetime(2024, 1, 1, 2),
33+
datetime.datetime(2024, 1, 1, 3),
34+
datetime.datetime(2024, 1, 1, 4),
35+
]
36+
37+
result = estimate_timestep(dates)
38+
assert result == Hour
39+
40+
def test_estimate_monthly(self):
41+
"""Test estimating monthly timesteps."""
42+
# Monthly timesteps
43+
dates = [
44+
datetime.datetime(2024, 1, 1),
45+
datetime.datetime(2024, 2, 1),
46+
datetime.datetime(2024, 3, 1),
47+
datetime.datetime(2024, 4, 1),
48+
datetime.datetime(2024, 5, 1),
49+
]
50+
51+
result = estimate_timestep(dates)
52+
assert result == Month
53+
54+
def test_estimate_yearly(self):
55+
"""Test estimating yearly timesteps."""
56+
# Yearly timesteps
57+
dates = [
58+
datetime.datetime(2020, 1, 1),
59+
datetime.datetime(2021, 1, 1),
60+
datetime.datetime(2022, 1, 1),
61+
datetime.datetime(2023, 1, 1),
62+
datetime.datetime(2024, 1, 1),
63+
]
64+
65+
result = estimate_timestep(dates)
66+
assert result == Year
67+
68+
def test_estimate_dekadly(self):
69+
"""Test estimating dekadly timesteps."""
70+
# Dekadly timesteps (10-day periods)
71+
dates = [
72+
datetime.datetime(2024, 1, 1), # 1st dekad of Jan
73+
datetime.datetime(2024, 1, 11), # 2nd dekad of Jan
74+
datetime.datetime(2024, 1, 21), # 3rd dekad of Jan
75+
datetime.datetime(2024, 2, 1), # 1st dekad of Feb
76+
datetime.datetime(2024, 2, 11), # 2nd dekad of Feb
77+
]
78+
79+
result = estimate_timestep(dates)
80+
assert result == Dekad
81+
82+
def test_estimate_viirs(self):
83+
"""Test estimating VIIRS/MODIS timesteps."""
84+
# 8-day VIIRS periods
85+
dates = [
86+
datetime.datetime(2024, 1, 1), # DOY 1
87+
datetime.datetime(2024, 1, 9), # DOY 9
88+
datetime.datetime(2024, 1, 17), # DOY 17
89+
datetime.datetime(2024, 1, 25), # DOY 25
90+
datetime.datetime(2024, 2, 2), # DOY 33
91+
]
92+
93+
result = estimate_timestep(dates)
94+
assert result == ViirsModisTimeStep
95+
96+
def test_estimate_with_few_samples(self):
97+
"""Test that estimation works with minimal sample size."""
98+
# Just 2 dates
99+
dates = [
100+
datetime.datetime(2024, 1, 1),
101+
datetime.datetime(2024, 1, 2),
102+
]
103+
104+
result = estimate_timestep(dates)
105+
assert result == Day
106+
107+
def test_estimate_with_single_sample(self):
108+
"""Test that single sample returns None."""
109+
dates = [datetime.datetime(2024, 1, 1)]
110+
111+
result = estimate_timestep(dates)
112+
assert result is None
113+
114+
def test_estimate_with_empty_list(self):
115+
"""Test that empty list returns None."""
116+
dates = []
117+
118+
result = estimate_timestep(dates)
119+
assert result is None
120+
121+
def test_estimate_unsorted_dates(self):
122+
"""Test that unsorted dates are handled."""
123+
# Unsorted daily dates
124+
dates = [
125+
datetime.datetime(2024, 1, 3),
126+
datetime.datetime(2024, 1, 1),
127+
datetime.datetime(2024, 1, 4),
128+
datetime.datetime(2024, 1, 2),
129+
]
130+
131+
result = estimate_timestep(dates)
132+
assert result == Day
133+
134+
def test_estimate_with_irregular_spacing(self):
135+
"""Test estimation with some irregular spacing."""
136+
# Mostly monthly but with one different
137+
dates = [
138+
datetime.datetime(2024, 1, 1),
139+
datetime.datetime(2024, 2, 1),
140+
datetime.datetime(2024, 3, 1),
141+
datetime.datetime(2024, 4, 1),
142+
datetime.datetime(2024, 4, 15), # Irregular
143+
datetime.datetime(2024, 5, 1),
144+
datetime.datetime(2024, 6, 1),
145+
]
146+
147+
# Should still identify as monthly (mode = ~30 days)
148+
result = estimate_timestep(dates)
149+
assert result == Month
150+
151+
def test_estimate_monthly_end_of_month(self):
152+
"""Test monthly estimation with end-of-month dates."""
153+
# Monthly timesteps at month ends
154+
dates = [
155+
datetime.datetime(2024, 1, 31),
156+
datetime.datetime(2024, 2, 29), # Leap year
157+
datetime.datetime(2024, 3, 31),
158+
datetime.datetime(2024, 4, 30),
159+
]
160+
161+
result = estimate_timestep(dates)
162+
# Should recognize as monthly despite varying day counts
163+
assert result == Month
164+
165+
def test_estimate_dekad_vs_viirs(self):
166+
"""Test distinguishing between dekads and VIIRS."""
167+
# Dekads (10-day) - but note Feb 28 would trigger Dekad
168+
dates_dekad = [
169+
datetime.datetime(2024, 1, 1),
170+
datetime.datetime(2024, 1, 11),
171+
datetime.datetime(2024, 1, 21),
172+
datetime.datetime(2024, 2, 1),
173+
]
174+
175+
result = estimate_timestep(dates_dekad)
176+
assert result == Dekad
177+
178+
# VIIRS (8-day)
179+
dates_viirs = [
180+
datetime.datetime(2024, 1, 1),
181+
datetime.datetime(2024, 1, 9),
182+
datetime.datetime(2024, 1, 17),
183+
]
184+
185+
result = estimate_timestep(dates_viirs)
186+
assert result == ViirsModisTimeStep
187+
188+
def test_estimate_leap_year_considerations(self):
189+
"""Test estimation considers leap years."""
190+
# Yearly with leap year
191+
dates = [
192+
datetime.datetime(2020, 1, 1), # Leap year
193+
datetime.datetime(2021, 1, 1), # 366 days later
194+
datetime.datetime(2022, 1, 1), # 365 days later
195+
]
196+
197+
result = estimate_timestep(dates)
198+
# Should still recognize as yearly despite different day counts
199+
assert result == Year
200+
201+
def test_estimate_unknown_frequency(self):
202+
"""Test that unknown frequencies return None."""
203+
# 3-day frequency (not recognized)
204+
dates = [
205+
datetime.datetime(2024, 1, 1),
206+
datetime.datetime(2024, 1, 4),
207+
datetime.datetime(2024, 1, 7),
208+
datetime.datetime(2024, 1, 10),
209+
]
210+
211+
result = estimate_timestep(dates)
212+
assert result is None
213+
214+
def test_estimate_very_short_intervals(self):
215+
"""Test with sub-hourly intervals."""
216+
# 30-minute intervals (not hourly)
217+
dates = [
218+
datetime.datetime(2024, 1, 1, 0, 0),
219+
datetime.datetime(2024, 1, 1, 0, 30),
220+
datetime.datetime(2024, 1, 1, 1, 0),
221+
datetime.datetime(2024, 1, 1, 1, 30),
222+
]
223+
224+
result = estimate_timestep(dates)
225+
# Should return None for unsupported frequency
226+
assert result is None
227+
228+
def test_estimate_mixed_months_28_31_days(self):
229+
"""Test monthly detection with varying month lengths."""
230+
# Months with 28, 30, 31 days
231+
dates = [
232+
datetime.datetime(2023, 12, 1), # 31 days
233+
datetime.datetime(2024, 1, 1), # 31 days
234+
datetime.datetime(2024, 2, 1), # 29 days (leap)
235+
datetime.datetime(2024, 3, 1), # 31 days
236+
datetime.datetime(2024, 4, 1), # 30 days
237+
]
238+
239+
result = estimate_timestep(dates)
240+
# Most common interval should be ~30 days, identifying as Month
241+
assert result == Month
242+
243+
244+
class TestEstimateTimestepEdgeCases:
245+
"""Test suite for edge cases in timestep estimation."""
246+
247+
def test_estimate_with_duplicates(self):
248+
"""Test estimation with duplicate timestamps."""
249+
dates = [
250+
datetime.datetime(2024, 1, 1),
251+
datetime.datetime(2024, 1, 1), # Duplicate
252+
datetime.datetime(2024, 1, 2),
253+
datetime.datetime(2024, 1, 3),
254+
]
255+
256+
# Should handle duplicates (diff = 0)
257+
result = estimate_timestep(dates)
258+
# Depending on mode calculation, might be Day or None
259+
assert result is not None
260+
261+
def test_estimate_all_same_date(self):
262+
"""Test estimation when all dates are the same."""
263+
dates = [
264+
datetime.datetime(2024, 1, 1),
265+
datetime.datetime(2024, 1, 1),
266+
datetime.datetime(2024, 1, 1),
267+
]
268+
269+
result = estimate_timestep(dates)
270+
# All differences are 0, can't estimate
271+
assert result is None
272+
273+
def test_estimate_large_sample(self):
274+
"""Test estimation with large sample."""
275+
# 100 daily dates
276+
base = datetime.datetime(2024, 1, 1)
277+
dates = [base + datetime.timedelta(days=i) for i in range(100)]
278+
279+
result = estimate_timestep(dates)
280+
assert result == Day
281+
282+
def test_estimate_sparse_yearly(self):
283+
"""Test yearly estimation with sparse sampling."""
284+
dates = [
285+
datetime.datetime(2020, 6, 15),
286+
datetime.datetime(2021, 6, 15),
287+
datetime.datetime(2022, 6, 15),
288+
]
289+
290+
result = estimate_timestep(dates)
291+
assert result == Year

0 commit comments

Comments
 (0)