Functional Weave
Code in Python

property.rent-proration@1.0.1

impl/typescript.ts

2,069 bytes · the TypeScript 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.

import { civilFromDays, daysInMonth, epochDayFromIso } from "./dates_add_days.ts";  ← from dates.add-days ^1.0.0 · built alongside by fune
import { roundDiv } from "./math_round_div.ts";  ← from math.round-div ^1.0.0 · built alongside by fune
import { type Money, money } from "./money_amount.ts";  ← from money.amount ^1.0.0 · built alongside by fune
import { type RentProrationMethod } from "./property_rent_proration_types.ts";

const MAX_MINOR = 1_000_000_000;
// lcm(28, 29, 30, 31): every month's daily rate is a whole number of these parts.
const MONTH_PARTS = 377_580;

/**
 * Rent for the days fromDate to toDate 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.
 */
export function prorateRent(monthlyRent: Money, fromDate: string, toDate: string, method: RentProrationMethod): Money {
  if (!Number.isInteger(monthlyRent.minor) || Math.abs(monthlyRent.minor) > MAX_MINOR) {
    throw new RangeError(`monthlyRent must be at most ${MAX_MINOR} minor units either way, received ${monthlyRent.minor}`);
  }
  const start = epochDayFromIso(fromDate);
  const end = epochDayFromIso(toDate);
  if (end < start) {
    throw new RangeError(`toDate ${toDate} is before fromDate ${fromDate}`);
  }
  const days = end - start + 1;
  if (days > 366) {
    throw new RangeError(`the period must be at most 366 days, received ${days}`);
  }
  if (method === "annual-daily") {
    return money(roundDiv(monthlyRent.minor * 12 * days, 365, "half-up"), monthlyRent.currency);
  }
  if (method !== "calendar-month") {
    throw new RangeError(`unknown method "${method}": use annual-daily or calendar-month`);
  }
  // Each day is worth MONTH_PARTS / (days in its month) parts of a month.
  let parts = 0;
  let day = start;
  while (day <= end) {
    const date = civilFromDays(day);
    const length = daysInMonth(date.year, date.month);
    const segmentEnd = Math.min(end, day + (length - date.day));
    parts += (segmentEnd - day + 1) * (MONTH_PARTS / length);
    day = segmentEnd + 1;
  }
  return money(roundDiv(monthlyRent.minor * parts, MONTH_PARTS, "half-up"), monthlyRent.currency);
}