daterange with a month step raises 'day is out of range for month' when start is on the 29th-31st
timeutils.daterange with a month-based step raises ValueError: day is out of range for month whenever start falls on a day that does not exist in a later month (the 29th–31st). A monthly sequence anchored to a month-end date is a normal thing to ask for, and it crashes instead of yielding.
Repro (current main)
from datetime import date
from boltons.timeutils import daterange
list(daterange(date(2020, 1, 31), date(2020, 6, 30), step=(0, 1, 0), inclusive=True))
# ValueError: day is out of range for monthSame for date(2020, 3, 31), date(2021, 1, 31), date(2020, 1, 30), etc. — any month-step that lands the anchor day on a shorter month. It raises on the very first advance, so the generator yields nothing usable.
Cause
_advance keeps the day fixed and only replaces year and month:
def _advance(cur):
if m_step:
m_y_step, cur_month = divmod((cur.month - 1) + m_step, 12)
cur = cur.replace(year=cur.year + m_y_step,
month=(cur_month + 1)) # <- day is carried over unchanged
return cur + d_stepdate(2020, 1, 31).replace(month=2) is date(2020, 2, 31), which does not exist, so replace raises. There is no day validation or clamping.
Which behaviour do you want?
The crash is clearly a bug, but the fix is a design choice, so I would rather ask than presume:
- Clamp to the last valid day of the target month (what
dateutil.relativedeltadoes):Jan 31 -> Feb 29 -> Mar 29 -> .... Simple, never raises. Downside: after the first clamp the day "drifts" (it does not snap back to the 31st). - Snap to month-end when the anchor was a month-end:
Jan 31 -> Feb 29 -> Mar 31 -> Apr 30 -> .... Matches what most people mean by "the last of every month", but more logic and only well-defined when the anchor is itself a month-end. - Raise a clear, documented error (e.g. "daterange month step cannot start from day 29-31") instead of the current opaque
dateerror, and document the limitation.
My inclination is option 1 for its simplicity and precedent, with the drift called out in the docstring; option 2 if you would rather preserve month-end anchoring. Happy to open a PR with tests for whichever you prefer (or the clear-error version).
For context: existing daterange issues #319 (month 12 / year-step off-by-one), #297 (infinite loop when start == stop) and PR #432 (non-advancing step) are all separate; none touch the day-overflow path.
Source: mahmoud/boltons