impl/typescript.ts
1,723 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 RentalYield } from "./property_rental_yield_types.ts";
// Above this, amount * 10000 is no longer exact as a JavaScript number.
const MAX_MINOR = 90_000_000_000;
/**
* Gross and net rental yield in basis points. Each is rounded once, half-up,
* from the exact ratio, never from a rounded percentage.
*/
export function rentalYield(annualRent: Money, annualCosts: Money, propertyValue: Money): RentalYield {
const currency = propertyValue.currency;
for (const other of [annualRent, annualCosts]) {
if (other.currency !== currency) {
throw new RangeError(`currency mismatch: ${other.currency} and ${currency}`);
}
}
if (!Number.isInteger(propertyValue.minor) || propertyValue.minor <= 0) {
throw new RangeError(`propertyValue must be greater than zero, received ${propertyValue.minor}`);
}
if (!Number.isInteger(annualRent.minor) || annualRent.minor < 0) {
throw new RangeError(`annualRent must not be negative, received ${annualRent.minor}`);
}
if (!Number.isInteger(annualCosts.minor) || annualCosts.minor < 0) {
throw new RangeError(`annualCosts must not be negative, received ${annualCosts.minor}`);
}
for (const m of [annualRent.minor, annualCosts.minor, propertyValue.minor]) {
if (m > MAX_MINOR) throw new RangeError(`amounts must be at most ${MAX_MINOR} minor units`);
}
const net = annualRent.minor - annualCosts.minor;
return {
grossBasisPoints: roundDiv(annualRent.minor * 10000, propertyValue.minor, "half-up"),
netBasisPoints: roundDiv(net * 10000, propertyValue.minor, "half-up"),
netIncome: money(net, currency),
};
}