from .dates_add_days import civil_from_days, days_in_month, epoch_day_from_iso from .math_round_div import round_div from .money_amount import Money, money 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)