impl/typescript.ts
3,053 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 { daysBetween } from "./dates_days_between.ts"; ← from dates.days-between ^1.0.0 · built alongside by fune
import { climateChangeLevy } from "./energy_climate_change_levy.ts"; ← from energy.climate-change-levy ^1.0.0 · built alongside by fune
import { gasKwh } from "./energy_gas_kwh.ts"; ← from energy.gas-kwh ^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 { addVat } from "./finance_tax_add_vat.ts"; ← from finance.tax.add-vat ^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 EnergyBill, type EnergyBillInput } from "./energy_bill_calculate_types.ts";
// watt-hours x thousandths of a penny per kWh is millionths of a penny.
const DIVISOR = 1_000_000;
function checkRate(what: string, value: number): void {
if (!Number.isInteger(value) || value < 0) {
throw new RangeError(`${what} must be a whole number of thousandths of a penny, 0 or more, received ${value}`);
}
}
function exact(product: number): number {
if (product > Number.MAX_SAFE_INTEGER) {
throw new RangeError("energy charge too large to calculate exactly");
}
return product;
}
/**
* A GB electricity or gas bill from two reads: energy at the unit rate,
* standing charge by the day, Climate Change Levy unless the supply is for
* qualifying use, and VAT on the lot at the caller's category.
*/
export function calculateEnergyBill(input: EnergyBillInput, mode: RoundingMode): EnergyBill {
if (input.fuel !== "electricity" && input.fuel !== "gas") {
throw new RangeError(`unknown fuel "${input.fuel}": expected electricity or gas`);
}
checkRate("unitRate", input.unitRate);
checkRate("standingCharge", input.standingCharge);
const units = meterAdvance(input.openingRead.value, input.closingRead.value, input.meterDigits);
const days = daysBetween(input.openingRead.date, input.closingRead.date);
if (days < 0) {
throw new RangeError(
`the closing read must not be dated before the opening read, received ${input.openingRead.date} and ${input.closingRead.date}`
);
}
let wattHours: number;
if (input.fuel === "gas") {
if (input.gasUnit === null || input.calorificValue === null) {
throw new RangeError("a gas bill needs gasUnit and calorificValue");
}
wattHours = gasKwh(units, input.gasUnit, input.calorificValue).wattHours;
} else {
if (input.gasUnit !== null || input.calorificValue !== null) {
throw new RangeError("gasUnit and calorificValue are for gas bills only");
}
wattHours = units * 1000;
}
const energy = roundDiv(exact(wattHours * input.unitRate), DIVISOR, mode);
const standing = roundDiv(exact(days * input.standingCharge), 1000, mode);
const ccl = climateChangeLevy(input.fuel, wattHours, input.closingRead.date, input.qualifyingUse, mode).levy.minor;
const vat = addVat(money(energy + standing + ccl, "GBP"), "GB", input.vatCategory, input.closingRead.date);
return {
units,
wattHours,
days,
energyCharge: money(energy, "GBP"),
standingCharge: money(standing, "GBP"),
climateChangeLevy: money(ccl, "GBP"),
net: vat.net,
vat: vat.tax,
vatBasisPoints: vat.basisPoints,
total: vat.gross,
};
}