Skip to content

Commit 17c616d

Browse files
committed
add totuple method and tests
1 parent c529629 commit 17c616d

File tree

5 files changed

+847
-1
lines changed

5 files changed

+847
-1
lines changed

DateIntervalCycler/DateIntervalCycler.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,12 @@ class DateIntervalCycler:
137137
only_start=False, only_end=False, step=1) -> list[Union[dt.datetime, tuple[dt.datetime, dt.datetime]]]:
138138
Converts the intervals to a list.
139139
140+
totuple(start_override=None, end_override=None, from_current_position=False
141+
) -> list[Union[dt.datetime, tuple[dt.datetime, dt.datetime]]]:
142+
Return a tuple containing the first_interval_start, and then all the remaining
143+
interval_end dates (including last_interval_end).
144+
Note: `cid.totuple() == tuple(cid.tolist(only_start=True)) + (cid.last_interval_end,)`
145+
140146
set_first_interval_start(first_interval_start, start_before_first_interval=False):
141147
Sets the start date of the interval range. Changing the start date invokes reset().
142148
@@ -1194,6 +1200,79 @@ def _tolist_intervals(self, only_start: bool, only_end: bool, step: int):
11941200
lst.append(self.interval)
11951201
return lst
11961202

1203+
def totuple(
1204+
self,
1205+
start_override: Union[None, dt.datetime, dt.date, int] = None,
1206+
end_override: Union[None, dt.datetime, dt.date, int] = None,
1207+
from_current_position: bool = False,
1208+
) -> tuple[dt.datetime, ...]:
1209+
"""
1210+
Return a tuple containing the first_interval_start, and then all
1211+
the remaining interval_end dates (including last_interval_end).
1212+
1213+
The tuple can use the current interval as the start or use all the intervals.
1214+
The tuple can optionally specify a different starting and/or ending date.
1215+
If last_interval_end is None, then end_override must be specified,
1216+
so there is an end to the tuple.
1217+
1218+
Note, both the end date and end_override dates are inclusive.
1219+
1220+
Note2, self.totuple() == tuple(self.tolist(only_start=True)) + (self.last_interval_end,)
1221+
len(self.totuple()) == len(self.tolist()) + 1
1222+
1223+
Args:
1224+
start_override (Union[None, dt.datetime, dt.date, int], optional): Override for the start date of the list.
1225+
If int, then the interval at index is the
1226+
start of the list. Defaults to None.
1227+
end_override (Union[None, dt.datetime, dt.date, int], optional): Override for the end date of the list.
1228+
If int, then the (index-1) interval is the
1229+
end of the list. Defaults to None.
1230+
from_current_position (bool, optional): Flag to start list from current interval. Defaults to False.
1231+
1232+
1233+
Returns:
1234+
tuple[dt.datetime, ...]: The DateIntervalCycler date series as a tuple.
1235+
"""
1236+
1237+
if not self._has_last_end_date and end_override is None:
1238+
raise ValueError(
1239+
"\nDateIntervalCycler.totuple must specify an ending date\n"
1240+
"either when initializing the DateIntervalCycler object with `end=` or\n"
1241+
"or by passing `end_override` into this function."
1242+
)
1243+
1244+
cid = self.copy() # No reset, but do a shallow copy
1245+
1246+
if start_override is not None:
1247+
if isinstance(start_override, int):
1248+
cid.set_first_interval_start(self[start_override][0])
1249+
else:
1250+
cid.set_first_interval_start(start_override)
1251+
1252+
elif not from_current_position:
1253+
cid.reset()
1254+
1255+
if end_override is not None:
1256+
if isinstance(end_override, int):
1257+
end_override = self[end_override][0]
1258+
elif type(end_override) is not dt.datetime:
1259+
try:
1260+
end_override = dt.datetime(end_override.year, end_override.month, end_override.day)
1261+
except AttributeError:
1262+
raise ValueError(
1263+
"\nDateIntervalCycler.set_last_interval_end: Invalid last_interval_end.\n"
1264+
f"Received: {end_override}"
1265+
)
1266+
cid._last_end_date = end_override
1267+
cid._has_last_end_date = True
1268+
cid._len = -999 # no need to recalculate for dummy variable
1269+
if end_override <= cid._p0_date:
1270+
cid._at_last_interval = 1
1271+
if end_override <= cid._first_start_date:
1272+
return ()
1273+
1274+
return tuple(it for it in cid.iter(only_start=True)) + (cid._last_end_date,)
1275+
11971276
def _to_datetime(self, p, y: Optional[int] = None, feb29_move_next_fix=True) -> dt.datetime:
11981277
"""
11991278
Internal method that converts a cycle index to a datetime object using current interval's

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ content-type = "text/markdown"
4141
[tool.pytest.ini_options]
4242
# agressive parallel options
4343
addopts = "-ra --dist worksteal -nauto" # Defaults to parallel tests, comment out if you want serial.
44+
# addopts = "-ra -v --dist worksteal -nauto" # parallel run with verbose output.
4445
# addopts="-ra --dist worksteal -nauto --run-slow-skip" # run skip tests and skip subset tests
4546
# addopts="-ra --dist worksteal -nauto -m 'not slow'" # on run fast tests
4647
# addopts = "-ra --dist worksteal -nauto -q" # supress output during testing

tests/test_simple_edge_cases.py

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,178 @@ def test_large_date_range():
215215
assert len(intervals) == len(cid) == 100
216216

217217

218+
def test_leap_year_handling_len1_totuple():
219+
cid = DateIntervalCycler([(2, 29)], dt(2019, 1, 1), dt(2019, 2, 1))
220+
date_series = cid.totuple()
221+
assert len(date_series) == 1 + 1
222+
assert len(date_series) == len(cid) + 1
223+
assert date_series == tuple((dt(2019, 1, 1), dt(2019, 2, 1)))
224+
225+
cid = DateIntervalCycler([(2, 29)], dt(2020, 1, 1), dt(2020, 2, 1))
226+
date_series = cid.totuple()
227+
assert len(date_series) == 2
228+
assert len(date_series) == len(cid) + 1
229+
assert date_series == tuple((dt(2020, 1, 1), dt(2020, 2, 1)))
230+
231+
232+
def test_leap_year_handling_len2_totuple():
233+
cid = DateIntervalCycler([(2, 29)], dt(2019, 1, 1), dt(2020, 1, 1))
234+
date_series = cid.totuple()
235+
assert len(date_series) == 2 + 1
236+
assert len(date_series) == len(cid) + 1
237+
assert date_series == tuple(
238+
(dt(2019, 1, 1), dt(2019, 2, 28), dt(2020, 1, 1)),
239+
)
240+
cid = DateIntervalCycler([(2, 29)], dt(2020, 1, 1), dt(2021, 1, 1))
241+
date_series = cid.totuple()
242+
assert len(date_series) == 2 + 1
243+
assert len(date_series) == len(cid) + 1
244+
assert date_series == tuple(
245+
(dt(2020, 1, 1), dt(2020, 2, 29), dt(2021, 1, 1)),
246+
)
247+
248+
249+
def test_leap_year_handling_len3_totuple():
250+
cid = DateIntervalCycler([(2, 29)], dt(2019, 1, 1), dt(2021, 1, 1))
251+
date_series = cid.totuple()
252+
assert len(date_series) == 3 + 1
253+
assert len(date_series) == len(cid) + 1
254+
assert date_series == tuple(
255+
(dt(2019, 1, 1), dt(2019, 2, 28), dt(2020, 2, 29), dt(2021, 1, 1)),
256+
)
257+
258+
cid = DateIntervalCycler([(2, 29)], dt(2020, 1, 1), dt(2022, 1, 1))
259+
date_series = cid.totuple()
260+
assert len(date_series) == 3 + 1
261+
assert len(date_series) == len(cid) + 1
262+
assert date_series == tuple(
263+
(dt(2020, 1, 1), dt(2020, 2, 29), dt(2021, 2, 28), dt(2022, 1, 1)),
264+
)
265+
266+
267+
def test_leap_year_handling_len4_totuple():
268+
cid = DateIntervalCycler([(2, 29)], dt(2019, 1, 1), dt(2022, 1, 1))
269+
date_series = cid.totuple()
270+
assert len(date_series) == 4 + 1
271+
assert len(date_series) == len(cid) + 1
272+
assert date_series == tuple(
273+
(dt(2019, 1, 1), dt(2019, 2, 28), dt(2020, 2, 29), dt(2021, 2, 28), dt(2022, 1, 1)),
274+
)
275+
276+
277+
def test_leap_year_handling_len6_totuple():
278+
cid = DateIntervalCycler([(2, 29)], dt(2019, 1, 1), dt(2024, 1, 1))
279+
date_series = cid.totuple()
280+
assert len(date_series) == 6 + 1
281+
assert len(date_series) == len(cid) + 1
282+
assert date_series == tuple(
283+
(
284+
dt(2019, 1, 1),
285+
dt(2019, 2, 28),
286+
dt(2020, 2, 29),
287+
dt(2021, 2, 28),
288+
dt(2022, 2, 28),
289+
dt(2023, 2, 28),
290+
dt(2024, 1, 1),
291+
),
292+
)
293+
294+
295+
def test_leap_year_handling_len7_totuple():
296+
cid = DateIntervalCycler([(2, 29)], dt(2019, 1, 1), dt(2025, 1, 1))
297+
date_series = cid.totuple()
298+
assert len(date_series) == 7 + 1
299+
assert len(date_series) == len(cid) + 1
300+
assert date_series == tuple(
301+
(
302+
dt(2019, 1, 1),
303+
dt(2019, 2, 28),
304+
dt(2020, 2, 29),
305+
dt(2021, 2, 28),
306+
dt(2022, 2, 28),
307+
dt(2023, 2, 28),
308+
dt(2024, 2, 29),
309+
dt(2025, 1, 1),
310+
),
311+
)
312+
313+
314+
def test_leap_year_handling_len8_totuple():
315+
cid = DateIntervalCycler([(2, 29)], dt(2019, 1, 1), dt(2026, 1, 1))
316+
date_series = cid.totuple()
317+
assert len(date_series) == 8 + 1
318+
assert len(date_series) == len(cid) + 1
319+
assert date_series == tuple(
320+
(
321+
dt(2019, 1, 1),
322+
dt(2019, 2, 28),
323+
dt(2020, 2, 29),
324+
dt(2021, 2, 28),
325+
dt(2022, 2, 28),
326+
dt(2023, 2, 28),
327+
dt(2024, 2, 29),
328+
dt(2025, 2, 28),
329+
dt(2026, 1, 1),
330+
),
331+
)
332+
333+
334+
def test_leap_year_handling_totuple():
335+
cid = DateIntervalCycler([(2, 29)], dt(2019, 1, 1), dt(2021, 1, 1))
336+
date_series = cid.totuple()
337+
assert len(date_series) == 4
338+
assert len(date_series) == len(cid) + 1
339+
assert date_series == tuple(
340+
(dt(2019, 1, 1), dt(2019, 2, 28), dt(2020, 2, 29), dt(2021, 1, 1)),
341+
)
342+
343+
344+
def test_cycle_on_year_edge_totuple():
345+
cid = DateIntervalCycler([(1, 1), (12, 31)], dt(2020, 1, 1), dt(2021, 1, 1))
346+
date_series = cid.totuple()
347+
assert len(date_series) == 3
348+
assert len(date_series) == len(cid) + 1
349+
assert date_series == tuple(
350+
(dt(2020, 1, 1), dt(2020, 12, 31), dt(2021, 1, 1)),
351+
)
352+
353+
354+
def test_monthly_cycle_totuple():
355+
cid = DateIntervalCycler([(1, 15), (2, 15)], dt(2020, 1, 1), dt(2021, 1, 1))
356+
date_series = cid.totuple()
357+
assert len(date_series) == 4
358+
assert len(date_series) == len(cid) + 1
359+
assert date_series == tuple(
360+
(dt(2020, 1, 1), dt(2020, 1, 15), dt(2020, 2, 15), dt(2021, 1, 1)),
361+
)
362+
363+
364+
def test_one_day_cycle_totuple():
365+
cid = DateIntervalCycler([(1, 1)], dt(2020, 1, 1), dt(2020, 1, 2))
366+
date_series = cid.totuple()
367+
assert len(date_series) == 2
368+
assert date_series == tuple((dt(2020, 1, 1), dt(2020, 1, 2)))
369+
370+
cid = DateIntervalCycler([(1, 1), (1, 2)], dt(2020, 1, 1), dt(2020, 1, 3))
371+
date_series = cid.totuple()
372+
assert len(date_series) == 3
373+
assert date_series == tuple((dt(2020, 1, 1), dt(2020, 1, 2), dt(2020, 1, 3)))
374+
375+
376+
def test_large_date_range_totuple():
377+
cid = DateIntervalCycler([(1, 1)], dt(2000, 1, 1), dt(2100, 1, 1))
378+
date_series = cid.totuple()
379+
assert len(date_series) == len(cid) + 1 == 100 + 1
380+
381+
cid = DateIntervalCycler([(1, 1)], dt(2000, 1, 1), dt(2100, 1, 2))
382+
date_series = cid.totuple()
383+
assert len(date_series) == len(cid) + 1 == 101 + 1 # extra interval for (dt(2100, 1, 1), dt(2100, 1, 2))
384+
385+
cid = DateIntervalCycler([(1, 1)], dt(2000, 1, 30), dt(2100, 1, 1))
386+
date_series = cid.totuple()
387+
assert len(date_series) == len(cid) + 1 == 100 + 1
388+
389+
218390
def test_invalid_month():
219391
with pytest.raises(ValueError):
220392
DateIntervalCycler([(13, 1)], dt(2020, 1, 1), dt(2021, 1, 1))

tests/test_size_attribute.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ def test_size_attribute_cycle():
124124
for end in end_list:
125125
cid = DateIntervalCycler(cycles, start, end)
126126
assert cid.size == len(cid.tolist())
127+
assert cid.size == len(cid.totuple()) - 1
127128

128129

129130
def test_size_attribute_cycle_with_set():
@@ -134,4 +135,5 @@ def test_size_attribute_cycle_with_set():
134135
cid.set_first_interval_start(start)
135136
for end in end_list:
136137
cid.set_last_interval_end(end)
137-
assert len(cid) == len(cid.tolist()) # len(cid) == cid.size
138+
assert len(cid) == len(cid.tolist())
139+
assert len(cid) == len(cid.totuple()) - 1

0 commit comments

Comments
 (0)