impl/typescript.ts
2,717 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 { 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 RentReviewResult, type RentReviewRule } from "./property_rent_review_types.ts";
const MAX_RENT = 1_000_000_000;
const MAX_INDEX = 1_000_000;
function checkBasisPoints(name: string, value: number | null): void {
if (value === null) return;
if (!Number.isInteger(value) || value <= -10000 || value > 100000) {
throw new RangeError(`${name} must be an integer from -9999 to 100000, received ${value}`);
}
}
/**
* The rent after an index-linked review, held between an optional collar and
* cap, and not below the passing rent when upward-only. Every comparison is
* on exact fractions and the rent is rounded half-up once.
*/
export function rentReview(
passingRent: Money,
baseIndex: number,
reviewIndex: number,
collarBasisPoints: number | null,
capBasisPoints: number | null,
upwardOnly: boolean,
): RentReviewResult {
if (!Number.isInteger(passingRent.minor) || passingRent.minor < 0 || passingRent.minor > MAX_RENT) {
throw new RangeError(`passingRent must be between 0 and ${MAX_RENT} minor units, received ${passingRent.minor}`);
}
for (const [name, value] of [["baseIndex", baseIndex], ["reviewIndex", reviewIndex]] as const) {
if (!Number.isInteger(value) || value <= 0 || value > MAX_INDEX) {
throw new RangeError(`${name} must be an integer from 1 to ${MAX_INDEX}, received ${value}`);
}
}
checkBasisPoints("collarBasisPoints", collarBasisPoints);
checkBasisPoints("capBasisPoints", capBasisPoints);
if (collarBasisPoints !== null && capBasisPoints !== null && collarBasisPoints > capBasisPoints) {
throw new RangeError(`collar ${collarBasisPoints} is above cap ${capBasisPoints}`);
}
const p = passingRent.minor;
// The multiplier as a fraction num/den: the index ratio, or 1 + collar/cap.
let num = reviewIndex;
let den = baseIndex;
let rule: RentReviewRule = "index";
if (capBasisPoints !== null && reviewIndex * 10000 > baseIndex * (10000 + capBasisPoints)) {
num = 10000 + capBasisPoints;
den = 10000;
rule = "cap";
} else if (collarBasisPoints !== null && reviewIndex * 10000 < baseIndex * (10000 + collarBasisPoints)) {
num = 10000 + collarBasisPoints;
den = 10000;
rule = "collar";
}
if (upwardOnly && num < den) {
num = 1;
den = 1;
rule = "upward-only";
}
return {
newRent: money(roundDiv(p * num, den, "half-up"), passingRent.currency),
indexRent: money(roundDiv(p * reviewIndex, baseIndex, "half-up"), passingRent.currency),
indexChangeBasisPoints: roundDiv((reviewIndex - baseIndex) * 10000, baseIndex, "half-up"),
appliedBy: rule,
};
}