Functional Weave
Code in Python

property.rent-proration@1.0.1

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 civil_from_days, days_in_month, epoch_day_from_iso  ← from dates.add-days ^1.0.0 · built alongside by fune
from .math_round_div import 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
from .property_rent_proration_types import RentProrationMethod

_MAX_MINOR = 1_000_000_000
# lcm(28, 29, 30, 31): every month's daily rate is a whole number of these parts.
_MONTH_PARTS = 377_580


def prorate_rent(monthly_rent: Money, from_date: str, to_date: str, method: RentProrationMethod) -> Money:
    """Rent for the days from_date to to_date inclusive, from a monthly rent,
    as one exact fraction rounded half-up once. The daily rate is never
    rounded on its own, which is the usual source of a pound's error on a
    part-month invoice."""
    if abs(monthly_rent.minor) > _MAX_MINOR:
        raise ValueError(
            "monthlyRent must be at most %d minor units either way, received %r" % (_MAX_MINOR, monthly_rent.minor)
        )
    start = epoch_day_from_iso(from_date)
    end = epoch_day_from_iso(to_date)
    if end < start:
        raise ValueError("toDate %s is before fromDate %s" % (to_date, from_date))
    days = end - start + 1
    if days > 366:
        raise ValueError("the period must be at most 366 days, received %d" % (days,))
    if method == "annual-daily":
        return money(round_div(monthly_rent.minor * 12 * days, 365, "half-up"), monthly_rent.currency)
    if method != "calendar-month":
        raise ValueError('unknown method "%s": use annual-daily or calendar-month' % (method,))
    # Each day is worth _MONTH_PARTS / (days in its month) parts of a month.
    parts = 0
    day = start
    while day <= end:
        date = civil_from_days(day)
        length = days_in_month(date.year, date.month)
        segment_end = min(end, day + (length - date.day))
        parts += (segment_end - day + 1) * (_MONTH_PARTS // length)
        day = segment_end + 1
    return money(round_div(monthly_rent.minor * parts, _MONTH_PARTS, "half-up"), monthly_rent.currency)