Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { daysBetween } from "./dates_days_between.ts"; ← from dates.days-between ^1.0.0 · built alongside by fune
import { meterAdvance } from "./energy_meter_advance.ts"; ← from energy.meter-advance ^1.0.0 · built alongside by fune
import { type RoundingMode, roundDiv } from "./math_round_div.ts"; ← from math.round-div ^1.0.0 · built alongside by fune
import { money } from "./money_amount.ts"; ← from money.amount ^1.0.0 · built alongside by fune
import { type MeterRead, type WaterBill, type WaterTariff } from "./energy_water_bill_types.ts";
function checkRate(what: string, value: number): void {
if (!Number.isInteger(value) || value < 0) {
throw new RangeError(`${what} must be a whole number of 0 or more, received ${value}`);
}
}
function exact(product: number): number {
if (product > Number.MAX_SAFE_INTEGER) {
throw new RangeError("water charge too large to calculate exactly");
}
return product;
}
/**
* A metered water and sewerage bill: volumetric water, sewerage on the share
* returned to the sewer, and standing and surface water charges apportioned
* by days / 365, each line rounded once.
*/
export function waterBill(
openingRead: MeterRead,
closingRead: MeterRead,
meterDigits: number,
tariff: WaterTariff,
surfaceWaterDrains: boolean,
currency: string,
mode: RoundingMode
): WaterBill {
const cubicMetres = meterAdvance(openingRead.value, closingRead.value, meterDigits);
const days = daysBetween(openingRead.date, closingRead.date);
if (days < 0) {
throw new RangeError(`the closing read must not be dated before the opening read, received ${openingRead.date} and ${closingRead.date}`);
}
checkRate("waterVolumeRate", tariff.waterVolumeRate);
checkRate("sewerageVolumeRate", tariff.sewerageVolumeRate);
checkRate("waterStandingCharge", tariff.waterStandingCharge);
checkRate("sewerageStandingCharge", tariff.sewerageStandingCharge);
checkRate("surfaceWaterCharge", tariff.surfaceWaterCharge);
if (!Number.isInteger(tariff.returnToSewer) || tariff.returnToSewer < 0 || tariff.returnToSewer > 10000) {
throw new RangeError(`returnToSewer must be basis points from 0 to 10000, received ${tariff.returnToSewer}`);
}
const water = roundDiv(exact(cubicMetres * tariff.waterVolumeRate), 1000, mode);
const sewerage = roundDiv(exact(cubicMetres * tariff.sewerageVolumeRate * tariff.returnToSewer), 10_000_000, mode);
const waterStanding = roundDiv(exact(tariff.waterStandingCharge * days), 365, mode);
const sewerageStanding = roundDiv(exact(tariff.sewerageStandingCharge * days), 365, mode);
const surface = surfaceWaterDrains ? roundDiv(exact(tariff.surfaceWaterCharge * days), 365, mode) : 0;
return {
cubicMetres,
days,
waterVolumeCharge: money(water, currency),
sewerageVolumeCharge: money(sewerage, currency),
waterStandingCharge: money(waterStanding, currency),
sewerageStandingCharge: money(sewerageStanding, currency),
surfaceWaterCharge: money(surface, currency),
total: money(water + sewerage + waterStanding + sewerageStanding + surface, currency),
};
}