dateutil relativedelta month-end billing cycle math: working versions (tested 2026-10-16)
Compute calendar billing cycles from an anchor date with dateutil relativedelta, keeping month-end anchors on the real last day, and lock the behavior down offline with pytest.
passed6 passed · 0 failed2026-10-16 12:55 UTC- Status
- passed
- Last passed
- 2026-10-16 12:55 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38903329434
The code
Exactly the files that passed. Your agent can pull them with get_recipe("python-dateutil-relativedelta-billing") (install), or see them on GitHub.
python-dateutil==2.9.0.post0 pytest==9.1.1
from calendar import monthrange
from datetime import date
from dateutil.relativedelta import relativedelta
def is_month_end(d: date) -> bool:
return d.day == monthrange(d.year, d.month)[1]
def month_end(d: date) -> date:
return d + relativedelta(day=31)
def add_months(d: date, months: int) -> date:
return d + relativedelta(months=months)
def next_billing_date(anchor: date, n: int) -> date:
d = anchor + relativedelta(months=n)
if is_month_end(anchor):
d = month_end(d)
return d
def billing_cycle(anchor: date, n: int) -> tuple[date, date]:
start = next_billing_date(anchor, n)
end = next_billing_date(anchor, n + 1)
return start, end
from datetime import date
from billing import (
add_months,
billing_cycle,
is_month_end,
month_end,
next_billing_date,
)
def test_add_months_clamps_to_month_end():
assert add_months(date(2024, 1, 31), 1) == date(2024, 2, 29)
assert add_months(date(2023, 1, 31), 1) == date(2023, 2, 28)
def test_day31_means_last_day_not_literal_31():
assert month_end(date(2024, 2, 10)) == date(2024, 2, 29)
assert month_end(date(2023, 2, 10)) == date(2023, 2, 28)
def test_iterative_addition_drifts_off_month_end():
d = date(2024, 1, 31)
for _ in range(2):
d = add_months(d, 1)
assert d == date(2024, 3, 29)
assert not is_month_end(d)
def test_anchored_billing_keeps_month_end():
anchor = date(2024, 1, 31)
assert next_billing_date(anchor, 1) == date(2024, 2, 29)
assert next_billing_date(anchor, 2) == date(2024, 3, 31)
assert next_billing_date(anchor, 3) == date(2024, 4, 30)
def test_billing_cycle_bounds_are_half_open():
start, end = billing_cycle(date(2024, 1, 31), 1)
assert start == date(2024, 2, 29)
assert end == date(2024, 3, 31)
def test_non_month_end_anchor_is_preserved():
anchor = date(2024, 1, 15)
assert next_billing_date(anchor, 1) == date(2024, 2, 15)
assert next_billing_date(anchor, 2) == date(2024, 3, 15)
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
Click CLI for RRULE next run times, tested with freezegun and CliRunner
A Click command that computes the next run times of an RFC 5545 rule in any IANA zone with dateutil rrule and zoneinfo, with DST gaps normalized and time frozen with freezegun in tests.
Recurring schedules with dateutil rrule and zoneinfo DST handling, frozen with time-machine
Build daily wall-clock schedules in a named timezone with dateutil rrule and zoneinfo, then test DST gaps, overlaps and naive datetimes offline with time-machine.