Functional Weave
Code in TypeScript

fleet.emissions

Vehicle greenhouse gas emissions (grams CO2e) from fuel used or distance, using UK government DESNZ conversion factors.

1.0.1 · published 2026-10-03 by charlie · Anterra

Pinned by 23 tests, run in TypeScript, Python and Rust.

What it does

Greenhouse gas emissions, in grams of CO2 equivalent, for fuel a fleet burned or distance it drove, using the UK government's conversion factors for company reporting (published by DESNZ, the Department for Energy Security and Net Zero). `fleetEmissions("fuel-diesel", "100", "litres", 2025)` is 257,082 g.

## Which factor

For example

  • fleetEmissions(fuel-diesel, 100, litres, 2,025) → co2e grams 257,082, factor 2.57082, factor year 2,025, activity fuel-diesel, unit litres 100 litres of pump diesel in 2025 is 257.082 kg
  • fleetEmissions(fuel-petrol, 40.5, litres, 2,025) → co2e grams 83,801, factor 2.06916, factor year 2,025, activity fuel-petrol, unit litres 40.5 litres of petrol in 2025: 83,800.98 g rounds to 83,801
  • fleetEmissions(fuel-diesel, 1, litres, 2,023) → co2e grams 2,512, factor 2.51206, factor year 2,023, activity fuel-diesel, unit litres one litre of diesel in the 2023 set

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 fleetEmissions(activity: string, quantity: string, unit: EmissionsUnit, factorYear: number): FleetEmissions
activitystringa fuel (fuel-diesel, fuel-petrol, ...) or a vehicle class (car-medium-petrol, van-average-diesel, hgv-all-hgvs-laden-average, ...)
quantitystringplain decimal text, not negative: at most 15 significant digits and 9 decimal places
unitEmissionsUnitlitres for a fuel; km or miles for a vehicle class
factorYearintthe DESNZ factor set, 2023 to 2026: normally the year you are reporting
returnsFleetEmissionsscope 1 (direct, tailpipe) emissions, rounded half up to a whole gram

The types it declares, generated into your project

export type EmissionsUnit = "litres" | "km" | "miles";

/** The emissions and the factor that produced them, for the audit trail. */
export interface FleetEmissions {
  /** quantity times factor, rounded half up to the gram */
  readonly co2eGrams: number;
  /** the factor as published, kg CO2e per unit, to 5 decimal places */
  readonly factor: string;
  readonly factorYear: number;
  readonly activity: string;
  readonly unit: string;
}

Your code names it in one line, in the file that uses it

import { fleetEmissions } from "#fune/fleet.emissions@^1";
impl/typescript.ts · 66 lines · open · raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

import { GHG_FACTORS } from "./fleet_emissions_data.ts";  ← this capability’s own data, compiled from data/ghg-factors.json into the same file by fune build
import { type EmissionsUnit, type FleetEmissions } from "./fleet_emissions_types.ts";

const DECIMAL = /^(\d+)(?:\.(\d+))?$/;
const MAX_SAFE = 9007199254740991n;

function formatFactor(factor: number): string {
  const whole = Math.floor(factor / 100000);
  const fraction = String(factor % 100000).padStart(5, "0");
  return `${whole}.${fraction}`;
}

/**
 * Scope 1 emissions for fuel burned or distance driven, from the UK
 * government's conversion factors for company reporting.
 *
 * The quantity is decimal text and the factor an integer in 1e-5 kg, so the
 * product is exact and rounded once, to the gram. Multiplying floats instead
 * drifts in the last place and can round a half-gram the wrong way.
 */
export function fleetEmissions(activity: string, quantity: string, unit: EmissionsUnit, factorYear: number): FleetEmissions {
  if (unit !== "litres" && unit !== "km" && unit !== "miles") {
    throw new RangeError(`unknown unit "${unit}": use litres, km or miles`);
  }
  if (!Number.isInteger(factorYear)) {
    throw new RangeError(`factorYear must be a whole year, received ${factorYear}`);
  }
  const match = typeof quantity === "string" ? DECIMAL.exec(quantity) : null;
  if (match === null) {
    throw new RangeError(`quantity must be plain decimal text, not negative, received "${quantity}"`);
  }
  const fraction = match[2] ?? "";
  const digits = (match[1] + fraction).replace(/^0+/, "");
  if (digits.length > 15 || fraction.length > 9) {
    throw new RangeError(`quantity must have at most 15 significant digits and 9 decimal places, received "${quantity}"`);
  }

  let found: number | null = null;
  let yearKnown = false;
  let activityKnown = false;
  for (const row of GHG_FACTORS) {
    if (row.factorYear !== factorYear) continue;
    yearKnown = true;
    if (row.activity !== activity) continue;
    activityKnown = true;
    if (row.unit === unit) found = row.factor;
  }
  if (!yearKnown) {
    throw new RangeError(`no conversion factors for ${factorYear}: this package carries 2023 to 2026`);
  }
  if (!activityKnown) {
    throw new RangeError(`unknown activity "${activity}" in the ${factorYear} factors`);
  }
  if (found === null) {
    throw new RangeError(`no conversion factor for ${activity} per ${unit} in ${factorYear}`);
  }

  // grams = quantity x factor x 1e-5 kg x 1000 g/kg = quantity x factor / 100
  const numerator = BigInt(match[1] + fraction) * BigInt(found);
  const denominator = 10n ** BigInt(fraction.length) * 100n;
  const grams = (2n * numerator + denominator) / (2n * denominator);
  if (grams > MAX_SAFE) {
    throw new RangeError("the result exceeds 2^53 - 1 grams");
  }
  return { co2eGrams: Number(grams), factor: formatFactor(found), factorYear, activity, unit };
}

Install

fune build

With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and nothing else, 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 fleet.emissions
Download for TypeScript fleet.emissions-1.0.1-typescript.fune · 135,250 bytes sha256 2f0ee69eaaed66b35d3162fab5dbdaa4e0c656061a83645cdfa151ee92bda2b6

The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./fleet.emissions-1.0.1-typescript.fune, or fetch it from a terminal with fune pull fleet.emissions@1.0.1:typescript.

The whole function, every language, is one file too: fleet.emissions-1.0.1.fune, 142,757 bytes, sha256 889b6473407bbb10b8b415ea8a5d9fa9ddc9d09a15beacbd289c885db32cce5c. 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 fleet.emissions

after — your function gets the result and the arguments, and returns the final result.

// fune: after fleet.emissions

replace — it requires no other capability, so there is no dependency to replace.

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 fleet.emissions --steps.

// fune: step fleet.emissions 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.

CaseArgumentsExpected
100 litres of pump diesel in 2025 is 257.082 kg fuel-diesel, 100, litres, 2,025 → co2e grams 257,082, factor 2.57082, factor year 2,025, activity fuel-diesel, unit litres
40.5 litres of petrol in 2025: 83,800.98 g rounds to 83,801 fuel-petrol, 40.5, litres, 2,025 → co2e grams 83,801, factor 2.06916, factor year 2,025, activity fuel-petrol, unit litres
one litre of diesel in the 2023 set fuel-diesel, 1, litres, 2,023 → co2e grams 2,512, factor 2.51206, factor year 2,023, activity fuel-diesel, unit litres
one litre of diesel in the 2024 set fuel-diesel, 1, litres, 2,024 → co2e grams 2,513, factor 2.51279, factor year 2,024, activity fuel-diesel, unit litres
one litre of diesel in the 2026 set fuel-diesel, 1, litres, 2,026 → co2e grams 2,584, factor 2.58354, factor year 2,026, activity fuel-diesel, unit litres
an exact half gram rounds up: 0.1 litre of 2026 petrol is 207.5 g fuel-petrol, 0.1, litres, 2,026 → co2e grams 208, factor 2.07500, factor year 2,026, activity fuel-petrol, unit litres
1,000 km in an average petrol car, 2025 car-average-petrol, 1000, km, 2,025 → co2e grams 162,720, factor 0.16272, factor year 2,025, activity car-average-petrol, unit km
100 miles uses DESNZ's own per-mile factor car-average-petrol, 100, miles, 2,025 → co2e grams 26,187, factor 0.26187, factor year 2,025, activity car-average-petrol, unit miles
the same trip in km (160.9344) agrees to the gram car-average-petrol, 160.9344, km, 2,025 → co2e grams 26,187, factor 0.16272, factor year 2,025, activity car-average-petrol, unit km
250 km in an average laden HGV in 2026: 224,357.5 g rounds up hgv-all-hgvs-laden-average, 250, km, 2,026 → co2e grams 224,358, factor 0.89743, factor year 2,026, activity hgv-all-hgvs-laden-average, unit km
Show the other 13 tests
CaseArgumentsExpected
zero distance is zero car-average-petrol, 0, km, 2,025 → co2e grams 0, factor 0.16272, factor year 2,025, activity car-average-petrol, unit km
a nine-decimal quantity is accepted and rounds to nothing fuel-diesel, 0.000000001, litres, 2,025 → co2e grams 0, factor 2.57082, factor year 2,025, activity fuel-diesel, unit litres
a fuel has no per-km factor fuel-diesel, 10, km, 2,025 → error: no conversion factor for fuel-diesel per km in 2025
a vehicle class has no per-litre factor car-average-petrol, 10, litres, 2,025 → error: no conversion factor for car-average-petrol per litres in 2025
battery electric cars are not carried: their emissions are electricity, not scope 1 car-average-bev, 10, km, 2,025 → error: unknown activity "car-average-bev" in the 2025 factors
van PHEV factors start in 2024 van-average-phev, 10, km, 2,023 → error: unknown activity "van-average-phev" in the 2023 factors
2022 factors are not carried fuel-diesel, 10, litres, 2,022 → error: no conversion factors for 2022
a negative quantity is refused fuel-diesel, -5, litres, 2,025 → error: quantity must be plain decimal text, not negative
exponent notation is refused fuel-diesel, 1e3, litres, 2,025 → error: quantity must be plain decimal text, not negative
a number instead of text is refused fuel-diesel, 12, litres, 2,025 → error: quantity must be plain decimal text, not negative
ten decimal places are too many fuel-diesel, 0.0000000001, litres, 2,025 → error: at most 15 significant digits and 9 decimal places
an unknown unit is refused fuel-diesel, 10, gallons, 2,025 → error: unknown unit "gallons"
a result beyond 2^53 - 1 grams is refused fuel-diesel, 123456789012345, litres, 2,025 → error: the result exceeds 2^53 - 1 grams

More from the author

Fuel is the better measure when you have it (fuel cards, bunkered fuel): it already reflects how the vehicles were driven. Distance by vehicle class is the fallback. The factors are the **Scope 1** figures, direct tailpipe emissions in kg CO2e (CO2, CH4 and N2O together):

- fuels, per litre: `fuel-diesel` and `fuel-petrol` (UK forecourt fuel with its average biofuel blend), `fuel-diesel-mineral` and `fuel-petrol-mineral` (100% mineral), `fuel-lpg`; - cars by size, per km or mile: `car-{small|medium|large|average}-{diesel|petrol|hybrid|phev|cng|lpg|unknown}` where DESNZ publishes one (there is no small CNG or LPG car); - `motorbike-{small|medium|large|average}`; - vans: `van-{class-1|class-2|class-3|average}-{diesel|petrol}`, and `van-average-{cng|lpg|unknown|phev}` and `van-class-3-phev` (the van PHEV factors appear from 2024); - HGVs (all diesel): `hgv-{rigid-3.5-7.5t|rigid-7.5-17t|rigid-over-17t|all-rigids|artic-3.5-33t|artic-over-33t|all-artics|all-hgvs}-laden-{0|50|100|average}`.

Battery electric vehicles are deliberately absent: their Scope 1 factor is zero, and their emissions belong to the electricity used (Scope 2, DESNZ's "UK electricity for EVs" table), which this package does not carry. A plug-in hybrid's factor likewise covers only the fuel it burns.

Per-mile factors are DESNZ's own, not the per-km factor times 1.609344, so a report matches the published spreadsheet exactly.

## Which year

`factorYear` picks the DESNZ set: 2023, 2024, 2025 or 2026. DESNZ publishes each set in June for reporting periods in that year; use the set for the year you are reporting, and keep it for restatements. The sets are publications, not rules that expire, so the table has no effective columns and ships whole.

In the 2026 set DESNZ split HGVs into refrigerated and non-refrigerated and renamed the averages; the `hgv-*` codes read the non-refrigerated rows for 2026 (earlier years' "HGV (all diesel)" table is the non-refrigerated fleet, refrigerated HGVs having had their own table), so the codes mean the same vehicle in every year.

## Exactness

Each factor is stored as an integer in units of 0.00001 kg (10 mg): DESNZ publishes five decimal places. The 2023 flat file holds the unrounded values behind them (2.5120638845637586 for diesel); they are rounded half up to the five places shown in the published tables (2.51206). The quantity is decimal text, so quantity times factor is exact, and the only rounding is the final one, half up to a whole gram. Quantities are limited to 15 significant digits and 9 decimal places, and a result above 2^53 - 1 grams is an error, so all three languages agree.

## Relationship to energy.*

This package is self-contained. If an `energy.carbon-emissions` capability with the same DESNZ tables appears in the registry, a later version of this one should read its data instead of carrying a second copy.

## Sources

- DESNZ, "Greenhouse gas reporting: conversion factors 2023", flat file (ghg-conversion-factors-2023-flat-file-update.xlsx): https://www.gov.uk/government/publications/greenhouse-gas-reporting-conversion-factors-2023 - DESNZ, "Greenhouse gas reporting: conversion factors 2024", flat file v1.1 (ghg-conversion-factors-2024-FlatFormat_v1_1.xlsx): https://www.gov.uk/government/publications/greenhouse-gas-reporting-conversion-factors-2024 - DESNZ, "Greenhouse gas reporting: conversion factors 2025", flat file (ghg-conversion-factors-2025-flat-format.xlsx): https://www.gov.uk/government/publications/greenhouse-gas-reporting-conversion-factors-2025 - DESNZ, "Greenhouse gas reporting: conversion factors 2026", flat file, revised July 2026 (ghg-conversion-factors-2026-flat-format-revised.xlsx): https://www.gov.uk/government/publications/greenhouse-gas-reporting-conversion-factors-2026

Every row of `data/ghg-factors.json` names the spreadsheet path it came from (Level 1 / Level 2 / Level 3 / column), always the "kg CO2e" row of Scope 1.

## Notices

Contains public sector information licensed under the Open Government Licence v3.0 (https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/).

1.0.1 adds its attribution notices (NOTICE). The code and the tests are unchanged.

Files

PathBytes
NOTICE178
README.md4,562
data/ghg-factors.json106,856
impl/python.py2,788
impl/rust.rs4,419
impl/typescript.ts2,804
vectors.json6,107