energy.bill-calculate
Electricity or gas bill from two meter reads: energy, standing charge, Climate Change Levy and VAT.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 15 tests, run in TypeScript, Python and Rust.
What it does
A GB electricity or gas bill from the reads at the start and end of the period, as a supplier's bill sets it out:
1. **Units**: closing read less opening read, allowing for the register rolling over (`energy.meter-advance`). 2. **kWh**: electricity meters read kWh; gas units are converted with the calorific value and the 1.02264 correction factor (`energy.gas-kwh`), kept to the watt-hour. 3. **Energy charge**: kWh x unit rate, rounded once to a penny by `mode`. 4. **Standing charge**: days x daily charge, rounded once by `mode`. The days are those between the two read dates (`dates.days-between`). 5. **Climate Change Levy**, unless the supply is for qualifying use (`energy.climate-change-levy`, at the rate on the closing read date). 6. **VAT** on the sum of those three lines, CCL included (the levy is part of the price for VAT), via `finance.tax.add-vat` at the rate for `vatCategory` on the closing read date, rounded half-up.
For example
calculateEnergyBill(fuel electricity, opening read …, closing read …, meter digits 5, gas unit —, calorific value —, unit rate 26,371, standing charge 52,143, qualifying use true, vat category REDUCE…)→ units 350, watt hours 350,000, days 30, energy charge £92.30, standing charge £15.64, climate change levy £0.00, net £107.94, vat £5.40, vat basis points 5%, total £113.34 a domestic electricity month at the January 2026 cap rates less VAT, 5% VAT, no levycalculateEnergyBill(fuel electricity, opening read …, closing read …, meter digits 5, gas unit —, calorific value —, unit rate 26,371, standing charge 52,143, qualifying use true, vat category REDUCE…)→ units 350, watt hours 350,000, days 30, energy charge £92.29, standing charge £15.64, climate change levy £0.00, net £107.93, vat £5.40, vat basis points 5%, total £113.33 the same bill rounding its lines down; VAT still rounds half-upcalculateEnergyBill(fuel electricity, opening read …, closing read …, meter digits 5, gas unit —, calorific value —, unit rate 25,000, standing charge 60,000, qualifying use false, vat category STAND…)→ units 2,000, watt hours 2,000,000, days 30, energy charge £500.00, standing charge £18.00, climate change levy £16.02, net £534.02, vat £106.80, vat basis points 20%, total £640.82 a business electricity month: CCL at 0.801p/kWh and 20% VAT on top of it
The function
The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.
export function calculateEnergyBill(input: EnergyBillInput, mode: RoundingMode): EnergyBill
| input | EnergyBillInput | the reads, the tariff and how the supply is taxed |
| mode | RoundingMode | how the energy, standing charge and levy lines round to a whole penny |
| returns | EnergyBill |
The types it declares, generated into your project
export type BillFuel = "electricity" | "gas";
/** One meter read. */
export interface MeterRead {
readonly date: string;
/** the register: kWh for electricity, whole units for gas */
readonly value: number;
}
/** Everything a GB electricity or gas bill is calculated from. */
export interface EnergyBillInput {
readonly fuel: BillFuel;
readonly openingRead: MeterRead;
/** lower than openingRead means the meter rolled over */
readonly closingRead: MeterRead;
/** whole-unit digits on the register, 1 to 15 */
readonly meterDigits: number;
/** gas only: cubic-metres or hundreds-cubic-feet */
readonly gasUnit: GasVolumeUnit | null;
/** gas only: average for the period, thousandths of a MJ/m³ */
readonly calorificValue: number | null;
/** excluding VAT, thousandths of a penny per kWh: 24.567p is 24567 */
readonly unitRate: number;
/** excluding VAT, thousandths of a penny per day */
readonly standingCharge: number;
/** domestic, charity non-business or de minimis: no Climate Change Levy */
readonly qualifyingUse: boolean;
/** finance.tax.vat-rate category for GB: REDUCED (5%), STANDARD (20%), ZERO */
readonly vatCategory: string;
}
/** The bill, line by line, in GBP. */
export interface EnergyBill {
/** meter advance in the meter's own units */
readonly units: number;
/** energy billed, thousandths of a kWh */
readonly wattHours: number;
/** days between the read dates */
readonly days: number;
readonly energyCharge: Money;
readonly standingCharge: Money;
/** zero for a qualifying-use supply */
readonly climateChangeLevy: Money;
/** the three lines above */
readonly net: Money;
readonly vat: Money;
readonly vatBasisPoints: number;
readonly total: Money;
}
Your code names it in one line, in the file that uses it
import { calculateEnergyBill } from "#fune/energy.bill-calculate@^1";
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,
};
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 7 dependencies, pins them in fune.lock, downloads only the TypeScript package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:
fune add energy.bill-calculate
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./energy.bill-calculate-1.0.0-typescript.fune, or fetch it from a terminal with fune pull energy.bill-calculate@1.0.0:typescript.
The whole function, every language, is one file too: energy.bill-calculate-1.0.0.fune, 30,441 bytes, sha256 26df26352e87e74ee45faaeb0d4476e1f3adee5cf54461214c2330a9c1c38f4e. It installs into a project of any language.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
// fune: before energy.bill-calculate
after — your function gets the result and the arguments, and returns the final result.
// fune: after energy.bill-calculate
replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.
// fune: replace dates.days-between in energy.bill-calculate
// fune: replace energy.climate-change-levy in energy.bill-calculate
// fune: replace energy.gas-kwh in energy.bill-calculate
// fune: replace energy.meter-advance in energy.bill-calculate
// fune: replace finance.tax.add-vat in energy.bill-calculate
// fune: replace math.round-div in energy.bill-calculate
// fune: replace money.amount in energy.bill-calculate
step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show energy.bill-calculate --steps.
// fune: step energy.bill-calculate after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
| Case | Arguments | Expected | |
|---|---|---|---|
| a domestic electricity month at the January 2026 cap rates less VAT, 5% VAT, no levy | fuel electricity, opening read …, closing read …, meter digits 5, gas unit —, calorific value —, unit rate 26,371, standing charge 52,143, qualifying use true, vat category REDUCE… | → | units 350, watt hours 350,000, days 30, energy charge £92.30, standing charge £15.64, climate change levy £0.00, net £107.94, vat £5.40, vat basis points 5%, total £113.34 |
| the same bill rounding its lines down; VAT still rounds half-up | fuel electricity, opening read …, closing read …, meter digits 5, gas unit —, calorific value —, unit rate 26,371, standing charge 52,143, qualifying use true, vat category REDUCE… | → | units 350, watt hours 350,000, days 30, energy charge £92.29, standing charge £15.64, climate change levy £0.00, net £107.93, vat £5.40, vat basis points 5%, total £113.33 |
| a business electricity month: CCL at 0.801p/kWh and 20% VAT on top of it | fuel electricity, opening read …, closing read …, meter digits 5, gas unit —, calorific value —, unit rate 25,000, standing charge 60,000, qualifying use false, vat category STAND… | → | units 2,000, watt hours 2,000,000, days 30, energy charge £500.00, standing charge £18.00, climate change levy £16.02, net £534.02, vat £106.80, vat basis points 20%, total £640.82 |
| a domestic gas month on a metric meter at 39.5 MJ/m³ | fuel gas, opening read …, closing read …, meter digits 5, gas unit cubic-metres, calorific value 39,500, unit rate 5,990, standing charge 32,410, qualifying use true, vat category… | → | units 100, watt hours 1,122,063, days 30, energy charge £67.21, standing charge £9.72, climate change levy £0.00, net £76.93, vat £3.85, vat basis points 5%, total £80.78 |
| a business gas month on an imperial meter that rolled over, at the 2023 gas levy rate | fuel gas, opening read …, closing read …, meter digits 4, gas unit hundreds-cubic-feet, calorific value 39,000, unit rate 4,000, standing charge 30,000, qualifying use false, vat … | → | units 20, watt hours 627,049, days 30, energy charge £25.08, standing charge £9.00, climate change levy £4.21, net £38.29, vat £7.66, vat basis points 20%, total £45.95 |
| domestic electricity in October 2026 under the temporary zero rate | fuel electricity, opening read …, closing read …, meter digits 5, gas unit —, calorific value —, unit rate 26,320, standing charge 54,830, qualifying use true, vat category ZERO, … | → | units 300, watt hours 300,000, days 30, energy charge £78.96, standing charge £16.45, climate change levy £0.00, net £95.41, vat £0.00, vat basis points 0%, total £95.41 |
| a de minimis business supply: 5% VAT and no levy | fuel electricity, opening read …, closing read …, meter digits 5, gas unit —, calorific value —, unit rate 25,000, standing charge 40,000, qualifying use true, vat category REDUCE… | → | units 900, watt hours 900,000, days 30, energy charge £225.00, standing charge £12.00, climate change levy £0.00, net £237.00, vat £11.85, vat basis points 5%, total £248.85 |
| reads on the same day with no use: no standing days, nothing to pay | fuel electricity, opening read …, closing read …, meter digits 5, gas unit —, calorific value —, unit rate 25,000, standing charge 40,000, qualifying use false, vat category STAND… | → | units 0, watt hours 0, days 0, energy charge £0.00, standing charge £0.00, climate change levy £0.00, net £0.00, vat £0.00, vat basis points 20%, total £0.00 |
| a gas bill without a calorific value is refused | fuel gas, opening read …, closing read …, meter digits 5, gas unit cubic-metres, calorific value —, unit rate 5,990, standing charge 32,410, qualifying use true, vat category REDU… | → | error: a gas bill needs gasUnit and calorificValue |
| gas details on an electricity bill are refused | fuel electricity, opening read …, closing read …, meter digits 5, gas unit cubic-metres, calorific value 39,500, unit rate 26,371, standing charge 52,143, qualifying use true, vat… | → | error: gasUnit and calorificValue are for gas bills only |
Show the other 5 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| a closing read dated before the opening read is refused | fuel electricity, opening read …, closing read …, meter digits 5, gas unit —, calorific value —, unit rate 26,371, standing charge 52,143, qualifying use true, vat category REDUCE… | → | error: the closing read must not be dated before the opening read |
| an unknown VAT category is refused by the VAT table | fuel electricity, opening read …, closing read …, meter digits 5, gas unit —, calorific value —, unit rate 26,371, standing charge 52,143, qualifying use true, vat category DOMEST… | → | error: no VAT rule for GB/DOMESTIC on 2026-01-31 |
| a negative unit rate is refused | fuel electricity, opening read …, closing read …, meter digits 5, gas unit —, calorific value —, unit rate -1, standing charge 52,143, qualifying use true, vat category REDUCED, h… | → | error: unitRate must be a whole number of thousandths of a penny |
| an unknown fuel is refused | fuel oil, opening read …, closing read …, meter digits 5, gas unit —, calorific value —, unit rate 26,371, standing charge 52,143, qualifying use true, vat category REDUCED, half-… | → | error: unknown fuel "oil" |
| a business supply dated before the earliest levy rate is refused | fuel electricity, opening read …, closing read …, meter digits 5, gas unit —, calorific value —, unit rate 25,000, standing charge 40,000, qualifying use false, vat category STAND… | → | error: no Climate Change Levy rate for electricity on 2023-03-01 |
More from the author
## VAT: the category is the caller's
`finance.tax.vat-rate` has no energy-specific category, and this capability does not add one: it takes the GB category from the caller and looks the rate up by date, so it follows the rate table rather than hard-coding 5%.
- **REDUCED (5%)**: fuel and power for qualifying use: domestic, charity non-business, and business supplies within the de minimis limits (33 kWh a day of electricity, 145 kWh a day of gas). - **STANDARD (20%)**: other business supplies. - **ZERO**: domestic electricity in Great Britain from 1 October 2026 to 31 March 2027, under the temporary zero rate. `finance.tax.vat-rate` does not yet carry that as a dated rule for REDUCED, so pass ZERO for a qualifying electricity supply in that window; gas stays REDUCED.
`qualifyingUse` is asked separately because it also decides the Climate Change Levy, and because the same qualifying supply can be REDUCED or ZERO depending on the fuel and date. Keep the two consistent: a qualifying supply is REDUCED or ZERO, a non-qualifying one STANDARD.
## Rates
Unit rate and standing charge are **excluding VAT**, in thousandths of a penny, as business tariffs are quoted. Ofgem's price cap figures (`energy.price-cap`) include VAT; divide by 1.05 first.
## Not covered
A single rate for the whole period: split a bill across a tariff change, a CCL rate change (1 April) or a VAT change into two calls. Economy 7 and half-hourly pricing are `energy.tariff-time-of-use`.
## Sources
- HMRC, "VAT on fuel and power (VAT Notice 701/19)", https://www.gov.uk/guidance/vat-on-fuel-and-power-notice-70119 (qualifying use, de minimis limits; paragraph 2.8 on the Climate Change Levy). - HM Treasury / HMRC, "Temporary zero rate of VAT for domestic electricity in Great Britain", https://www.gov.uk/government/publications/temporary-zero-rate-of-vat-for-domestic-electricity-in-great-britain - The capabilities this one uses carry their own sources.
Files
| Path | Bytes |
|---|---|
| README.md | 2,955 |
| impl/python.py | 3,031 |
| impl/rust.rs | 5,413 |
| impl/typescript.ts | 3,053 |
| vectors.json | 9,150 |