property.service-charge-apportion@1.0.1
impl/typescript.ts
2,647 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 { lcm } from "./math_gcd_lcm.ts"; ← from math.gcd-lcm ^1.0.0 · built alongside by fune
import { allocate } from "./money_allocate.ts"; ← from money.allocate ^1.0.0 · built alongside by fune
import { type Money } from "./money_amount.ts"; ← from money.amount ^1.0.0 · built alongside by fune
import { type ApportionBasis, type ServiceChargeLine, type ServiceChargeUnit } from "./property_service_charge_apportion_types.ts";
/**
* Share a service charge between units by lease fraction, floor area or fixed
* percentage. Every share becomes an integer weight, and money.allocate does
* the one split, so the lines always add up to the total exactly.
*/
export function apportionServiceCharge(
total: Money,
units: readonly ServiceChargeUnit[],
basis: ApportionBasis,
): readonly ServiceChargeLine[] {
if (basis !== "fraction" && basis !== "floor-area" && basis !== "percentage") {
throw new RangeError(`unknown basis "${basis}": use fraction, floor-area or percentage`);
}
if (units.length === 0) {
throw new RangeError("units must not be empty");
}
for (const u of units) {
if (!Number.isSafeInteger(u.share) || u.share < 0) {
throw new RangeError(`unit "${u.unit}": share must be a whole number, zero or more, received ${u.share}`);
}
if (basis === "fraction") {
if (u.of === null || !Number.isSafeInteger(u.of) || u.of <= 0) {
throw new RangeError(`unit "${u.unit}": a fraction needs a denominator "of" greater than zero`);
}
} else if (u.of !== null) {
throw new RangeError(`unit "${u.unit}": "of" is only for the fraction basis`);
}
}
let weights: number[];
if (basis === "fraction") {
let common = 1;
for (const u of units) common = lcm(common, u.of as number);
weights = units.map((u) => u.share * (common / (u.of as number)));
const sum = weights.reduce((a, b) => a + b, 0);
if (!Number.isSafeInteger(sum)) {
throw new RangeError("the fractions are too fine to apportion exactly");
}
if (sum !== common) {
throw new RangeError(`the fractions add up to ${sum}/${common}, not exactly 1`);
}
} else {
weights = units.map((u) => u.share);
const sum = weights.reduce((a, b) => a + b, 0);
if (basis === "percentage" && sum !== 10000) {
throw new RangeError(`the percentages add up to ${sum} basis points, not 10000`);
}
if (sum === 0) {
throw new RangeError("the floor areas add up to zero");
}
}
const sum = weights.reduce((a, b) => a + b, 0);
if (!Number.isSafeInteger(sum) || !Number.isSafeInteger(Math.abs(total.minor) * sum)) {
throw new RangeError("the fractions are too fine to apportion exactly");
}
const parts = allocate(total, weights);
return units.map((u, i) => ({ unit: u.unit, charge: parts[i] }));
}