Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
from .math_round_div import RoundingMode, round_div ← from math.round-div ^1.0.0 · built alongside by fune
from .money_amount import Money, money ← from money.amount ^1.0.0 · built alongside by fune
#: A hundred years of monthly payments, sixty of weekly ones.
MAX_PAYMENTS = 3000
def _is_int(value: object) -> bool:
return isinstance(value, int) and not isinstance(value, bool)
def payment_count(annual_rate_basis_points: int, term_months: int, payments_per_year: int) -> int:
"""The number of payments a term holds, after checking the arguments every
lending capability shares."""
if not _is_int(annual_rate_basis_points) or annual_rate_basis_points < 0 or annual_rate_basis_points > 100000:
raise ValueError(
"annualRateBasisPoints must be between 0 and 100000, received %r" % (annual_rate_basis_points,)
)
if not _is_int(payments_per_year) or payments_per_year < 1 or payments_per_year > 52:
raise ValueError("paymentsPerYear must be between 1 and 52, received %r" % (payments_per_year,))
if not _is_int(term_months) or term_months < 1:
raise ValueError("termMonths must be at least 1, received %r" % (term_months,))
if (term_months * payments_per_year) % 12 != 0:
raise ValueError(
"a term of %d months is not a whole number of payments at %d a year" % (term_months, payments_per_year)
)
n = term_months * payments_per_year // 12
if n > MAX_PAYMENTS:
raise ValueError("%d payments is more than the %d payment limit" % (n, MAX_PAYMENTS))
return n
def round_wide(numerator: int, denominator: int, mode: RoundingMode) -> int:
"""Round the exact quotient numerator / denominator (both positive) with a
math.round-div mode, however large they are.
Only the integer part and where the remainder sits against one half
matter, so the decision is handed to round_div as a small fraction with the
same integer part and the same side of the half: q, q + 1/4, q + 1/2 or
q + 3/4.
"""
q, remainder = divmod(numerator, denominator)
twice = remainder * 2
if twice == 0:
return round_div(q, 1, mode)
if twice == denominator:
return round_div(2 * q + 1, 2, mode)
return round_div(4 * q + (1 if twice < denominator else 3), 4, mode)
def loan_payment(
principal: Money,
annual_rate_basis_points: int,
term_months: int,
payments_per_year: int,
mode: RoundingMode,
) -> Money:
"""The level payment that repays ``principal`` over the term at a nominal
annual rate divided equally between the periods:
payment = P * r / (1 - (1 + r)^-n), r = rate / payments_per_year
With r = b / D (b basis points, D = 10000 * payments_per_year) this is the
exact fraction P * b * (D + b)^n / (D * ((D + b)^n - D^n)), evaluated in
integers of whatever size it takes and rounded once. A zero rate is P / n.
"""
if not _is_int(principal.minor) or principal.minor <= 0:
raise ValueError("principal must be greater than zero, received %r" % (principal.minor,))
n = payment_count(annual_rate_basis_points, term_months, payments_per_year)
if annual_rate_basis_points == 0:
return money(round_div(principal.minor, n, mode), principal.currency)
b = annual_rate_basis_points
d = 10000 * payments_per_year
grown = (d + b) ** n
numerator = principal.minor * b * grown
denominator = d * (grown - d ** n)
return money(round_wide(numerator, denominator, mode), principal.currency)