Functional Weave
Code in Python

education.fee-instalments@1.0.0

impl/python.py

1,978 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 typing import List, Sequence

from .dates_add_days import parse_iso_date  ← from dates.add-days ^1.0.0 · built alongside by fune
from .education_fee_instalments_types import FeeInstalment
from .money_allocate import allocate  ← from money.allocate ^1.0.0 · built alongside by fune
from .money_amount import Money, money  ← from money.amount ^1.0.0 · built alongside by fune


def _is_int(value: object) -> bool:
    return isinstance(value, int) and not isinstance(value, bool)


def fee_instalments(total: Money, shares: Sequence[int], due_dates: Sequence[str]) -> List[FeeInstalment]:
    """Split a fee into dated instalments that add up to it exactly.

    The split is money.allocate's: floors, then the leftover pennies to the
    largest remainders, ties to the earlier instalment.
    """
    if not _is_int(total.minor) or total.minor < 0:
        raise ValueError("total must not be negative, received %s" % (total.minor,))
    if len(shares) == 0:
        raise ValueError("shares must name at least one instalment")
    if len(shares) != len(due_dates):
        raise ValueError(
            "shares and dueDates must be the same length, received %s and %s" % (len(shares), len(due_dates))
        )
    for s in shares:
        if not _is_int(s) or s < 0:
            raise ValueError("each share must be a whole number of 0 or more, received %s" % (s,))
    if sum(shares) == 0:
        raise ValueError("shares must not all be zero")
    for i, d in enumerate(due_dates):
        parse_iso_date(d)
        if i > 0 and d <= due_dates[i - 1]:
            raise ValueError("dueDates must be in strictly ascending order: %s follows %s" % (d, due_dates[i - 1]))
    amounts = allocate(total, shares)
    paid = 0
    out: List[FeeInstalment] = []
    for i, amount in enumerate(amounts):
        paid += amount.minor
        out.append(
            FeeInstalment(
                number=i + 1,
                due_date=due_dates[i],
                amount=amount,
                cumulative=money(paid, total.currency),
                remaining=money(total.minor - paid, total.currency),
            )
        )
    return out