I've created a function to split a period into the biggest different time unit possible. It can be years
, months
, weeks
or days
.
However, it ended up being quite big and I feel it can be greatly improved, but I'm having difficulties finding ways to do it.
import calendar
from datetime import datetime, timedelta
def create_intervals(start_date, end_date):
start = datetime.strptime(start_date, "%Y-%m-%d")
end = datetime.strptime(end_date, "%Y-%m-%d")
di = []
# Year
if (sy := start.year) < (ey := end.year):
for y in range(sy, ey + 1):
if sy == y:
di.append((start_date, f"{y}-12-31"))
elif sy < y < ey:
di.append((f"{y}-01-01", f"{y}-12-31"))
else:
di.append((f"{y}-01-01", end_date))
# Month
elif (sm := start.month) < (em := end.month):
for m in range(sm, em + 1):
last_day = calendar.monthrange(sy, m)[1]
if sm == m:
di.append((start_date, f"{sy}-{m:02d}-{last_day}"))
elif sm < m < em:
di.append((f"{sy}-{m:02d}-01", f"{sy}-{m:02d}-{last_day}"))
else:
di.append((f"{sy}-{m:02d}-01", end_date))
# Week
elif (sw := start.isocalendar().week) < (ew := end.isocalendar().week):
for w in range(sw, ew + 1):
if sw == w:
last = start - timedelta(days=start.weekday()) + timedelta(days=6)
di.append((start_date, f"{sy}-{sm:02d}-{last.day:02d}"))
elif sw < w < ew:
wd = datetime.strptime(f"{sy}-W{w}-1", "%Y-W%W-%w")
first = wd - timedelta(days=wd.weekday())
last = wd - timedelta(days=wd.weekday()) + timedelta(days=6)
di.append((f"{sy}-{sm:02d}-{first.day:02d}", f"{sy}-{sm:02d}-{last.day:02d}"))
else:
first = end - timedelta(days=end.weekday())
di.append((f"{sy}-{sm:02d}-{first.day:02d}", end_date))
# Day
elif start.day < end.day:
for d in range(start.day, end.day + 1):
di.append((f"{sy}-{sm:02d}-{d:02d}", f"{sy}-{sm:02d}-{d:02d}"))
# None
else:
di.append((start_date, end_date))
return di
Example for years as biggest different time unit:
create_intervals("2021年10月22日", "2023年03月02日")
# Outputs intervals for each year:
# [("2021年10月22日", "2021年12月31日"),
# ("2022年01月01日", "2022年12月31日"),
# ("2023年01月01日", "2023年03月02日")]
Example for days as biggest different time unit:
create_intervals("2021年10月22日", "2021年10月24日")
# Outputs:
# [("2021年10月22日", "2021年10月22日"),
# ("2021年10月23日", "2021年10月23日"),
# ("2021年10月24日", "2021年10月24日")]
-
\$\begingroup\$ Could you also show an invocation example for weeks? \$\endgroup\$Reinderien– Reinderien2022年10月26日 23:36:11 +00:00Commented Oct 26, 2022 at 23:36
1 Answer 1
There is never a case where you should use string datetime representation in this code, and there is never a case where you should use datetime
; use date
instead.
Consider converting to a generator function to simplify your code.
Your one- and two-letter variable names need to go away.
Almost all of your walrus assignments need to go away except that of the week comparison.
first = wd - timedelta(days=wd.weekday())
should not do any subtraction because the right-hand term will always be 0.
Write unit tests based on the cases you already have, and expand for cases that you don't already have.
Writing conditions for the beginning and end of a sequence within a loop is an antipattern - instead, cut those out to statements before and after the loop, and make the loop unconditional.
Add PEP484 typehints.
Suggested
import calendar
from datetime import date, timedelta
from typing import Iterator
def create_intervals(start: date, end: date) -> Iterator[tuple[date, date]]:
# Year
if start.year < end.year:
yield start, date(start.year, 12, 31)
for year in range(start.year + 1, end.year):
yield date(year, 1, 1), date(year, 12, 31)
yield date(end.year, 1, 1), end
# Month
elif start.month < end.month:
last_days = (
calendar.monthrange(start.year, month)[1]
for month in range(start.month, end.month + 1)
)
yield start, date(start.year, start.month, next(last_days))
for month, last_day in zip(range(start.month + 1, end.month), last_days):
yield date(start.year, month, 1), date(start.year, month, last_day)
yield date(start.year, end.month, 1), end
# Week
elif (start_week := start.isocalendar().week) < (end_week := end.isocalendar().week):
last = start + timedelta(days=6 - start.weekday())
yield start, date(start.year, start.month, last.day)
for week in range(start_week + 1, end_week):
first = date.fromisocalendar(start.year, week, 1)
last = first + timedelta(days=6)
yield date(start.year, start.month, first.day), date(start.year, start.month, last.day)
first = end - timedelta(days=end.weekday())
yield date(start.year, start.month, first.day), end
# Day
elif start.day < end.day:
for day in range(start.day, end.day + 1):
yield date(start.year, start.month, day), date(start.year, start.month, day)
# None
else:
yield start, end
def test() -> None:
# Years
result = tuple(create_intervals(date(2021, 10, 22), date(2023, 3, 2)))
assert result == (
(date(2021, 10, 22), date(2021, 12, 31)),
(date(2022, 1, 1), date(2022, 12, 31)),
(date(2023, 1, 1), date(2023, 3, 2)),
)
# Months
result = tuple(create_intervals(date(2021, 3, 7), date(2021, 10, 26)))
assert result == (
(date(2021, 3, 7), date(2021, 3, 31)),
(date(2021, 4, 1), date(2021, 4, 30)),
(date(2021, 5, 1), date(2021, 5, 31)),
(date(2021, 6, 1), date(2021, 6, 30)),
(date(2021, 7, 1), date(2021, 7, 31)),
(date(2021, 8, 1), date(2021, 8, 31)),
(date(2021, 9, 1), date(2021, 9, 30)),
(date(2021, 10, 1), date(2021, 10, 26)),
)
# Weeks
result = tuple(create_intervals(date(2021, 10, 2), date(2021, 10, 26)))
assert result == (
(date(2021, 10, 2), date(2021, 10, 3)),
(date(2021, 10, 4), date(2021, 10, 10)),
(date(2021, 10, 11), date(2021, 10, 17)),
(date(2021, 10, 18), date(2021, 10, 24)),
(date(2021, 10, 25), date(2021, 10, 26))
)
# Days
result = tuple(create_intervals(date(2021, 10, 22), date(2021, 10, 24)))
assert result == (
(date(2021, 10, 22), date(2021, 10, 22)),
(date(2021, 10, 23), date(2021, 10, 23)),
(date(2021, 10, 24), date(2021, 10, 24)),
)
if __name__ == '__main__':
test()
-
1\$\begingroup\$ This was such an improvement! I really liked how converting to a generator function and using the unconditional loops simplified the whole structure. Thank you for taking the time \$\endgroup\$CIRCLE– CIRCLE2022年10月28日 01:13:59 +00:00Commented Oct 28, 2022 at 1:13