Functional Weave
Code in Python

subscriptions.next-billing-date@1.0.0

impl/python.py

1,971 bytes · the Python implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

from .dates_add_days import add_days, epoch_day_from_iso, parse_iso_date  ← from dates.add-days ^1.0.0 · built alongside by fune
from .dates_add_months import add_months  ← from dates.add-months ^1.0.0 · built alongside by fune
from .subscriptions_next_billing_date_types import BillingInterval

MONTHS = {"month": 1, "quarter": 3, "year": 12}
DAYS = {"day": 1, "week": 7}


def next_billing_date(anchor_date: str, interval: BillingInterval, interval_count: int, after_date: str) -> str:
    """The first billing date strictly after ``after_date``: anchor + k
    intervals for the smallest k >= 0 that lands after it. Each candidate is
    computed from the anchor, never from the previous billing date, so the
    31st stays on month ends instead of decaying to the 28th after February.
    """
    if isinstance(interval_count, bool) or not isinstance(interval_count, int) or interval_count < 1:
        raise ValueError("interval count must be 1 or more, received %r" % (interval_count,))
    if interval not in MONTHS and interval not in DAYS:
        raise ValueError(
            'unknown billing interval "%s": expected day, week, month, quarter or year' % (interval,)
        )
    anchor = epoch_day_from_iso(anchor_date)
    after = epoch_day_from_iso(after_date)
    if after < anchor:
        return anchor_date

    if interval in DAYS:
        step = DAYS[interval] * interval_count
        k = (after - anchor) // step + 1
        return add_days(anchor_date, k * step)

    # Jump straight to the period containing after_date's month; the clamp can
    # only put that candidate on or before after_date, so at most one or two
    # steps forward are needed from there.
    step = MONTHS[interval] * interval_count
    a = parse_iso_date(anchor_date)
    b = parse_iso_date(after_date)
    months_apart = (b.year - a.year) * 12 + (b.month - a.month)
    k = months_apart // step
    candidate = add_months(anchor_date, k * step)
    while epoch_day_from_iso(candidate) <= after:
        k += 1
        candidate = add_months(anchor_date, k * step)
    return candidate