Functional Weave
Code in Rust

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

  • fleet_emissions(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
  • fleet_emissions(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
  • fleet_emissions(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.

pub fn fleet_emissions(activity: &str, quantity: &str, unit: &str, factor_year: i64) -> 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
factor_yearintthe 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

// EmissionsUnit is a string in Rust, one of: "litres", "km", "miles".
// Parameters take it as &str and results hold it as String.

/// The emissions and the factor that produced them, for the audit trail.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FleetEmissions {
    /// quantity times factor, rounded half up to the gram
    pub co2e_grams: i64,
    /// the factor as published, kg CO2e per unit, to 5 decimal places
    pub factor: String,
    pub factor_year: i64,
    pub activity: String,
    pub unit: String,
}

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

fune!(fleet.emissions@^1);  // then call fleet_emissions(…)
impl/rust.rs · 121 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.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::fleet_emissions_data::GHG_FACTORS;  ← this capability’s own data, compiled from data/ghg-factors.json into the same file by fune build

const MAX_SAFE: i128 = 9007199254740991;

fn format_factor(factor: i64) -> String {
    format!("{}.{:05}", factor / 100000, factor % 100000)
}

/// Split plain decimal text into its whole and fractional digits, or None.
fn split_decimal(text: &str) -> Option<(&str, &str)> {
    let (whole, fraction) = match text.find('.') {
        Some(i) => (&text[..i], &text[i + 1..]),
        None => (text, ""),
    };
    let digits = |s: &str| s.bytes().all(|b| b.is_ascii_digit());
    if whole.is_empty() || !digits(whole) || !digits(fraction) || (text.contains('.') && fraction.is_empty()) {
        return None;
    }
    Some((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.
///
/// # Panics
/// Panics on an unknown unit, year or activity, a unit the activity has no
/// factor for, a malformed or negative quantity, or a result beyond 2^53 - 1.
pub fn fleet_emissions(activity: &str, quantity: &str, unit: &str, factor_year: i64) -> FleetEmissions {
    if unit != "litres" && unit != "km" && unit != "miles" {
        panic!("unknown unit \"{}\": use litres, km or miles", unit);
    }
    let (whole, fraction) = match split_decimal(quantity) {
        Some(parts) => parts,
        None => panic!(
            "quantity must be plain decimal text, not negative, received \"{}\"",
            quantity
        ),
    };
    let all_digits = format!("{}{}", whole, fraction);
    let significant = all_digits.trim_start_matches('0');
    if significant.len() > 15 || fraction.len() > 9 {
        panic!(
            "quantity must have at most 15 significant digits and 9 decimal places, received \"{}\"",
            quantity
        );
    }

    let mut found: Option<i64> = None;
    let mut year_known = false;
    let mut activity_known = false;
    for row in GHG_FACTORS {
        if row.factor_year != factor_year {
            continue;
        }
        year_known = true;
        if row.activity != activity {
            continue;
        }
        activity_known = true;
        if row.unit == unit {
            found = Some(row.factor);
        }
    }
    if !year_known {
        panic!("no conversion factors for {}: this package carries 2023 to 2026", factor_year);
    }
    if !activity_known {
        panic!("unknown activity \"{}\" in the {} factors", activity, factor_year);
    }
    let factor = match found {
        Some(f) => f,
        None => panic!("no conversion factor for {} per {} in {}", activity, unit, factor_year),
    };

    // grams = quantity x factor x 1e-5 kg x 1000 g/kg = quantity x factor / 100.
    // At most 15 digits times a factor below 1e7 fits i128 with room to spare.
    let value: i128 = if significant.is_empty() { 0 } else { significant.parse().unwrap() };
    let numerator = value * factor as i128;
    let denominator = 10i128.pow(fraction.len() as u32) * 100;
    let grams = (2 * numerator + denominator) / (2 * denominator);
    if grams > MAX_SAFE {
        panic!("the result exceeds 2^53 - 1 grams");
    }
    FleetEmissions {
        co2e_grams: grams as i64,
        factor: format_factor(factor),
        factor_year,
        activity: activity.to_string(),
        unit: unit.to_string(),
    }
}

pub fn fleet_emissions_to_value(e: &FleetEmissions) -> Value {
    Value::obj(vec![
        ("co2eGrams", Value::Int(e.co2e_grams)),
        ("factor", Value::str(&e.factor)),
        ("factorYear", Value::Int(e.factor_year)),
        ("activity", Value::str(&e.activity)),
        ("unit", Value::str(&e.unit)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let quantity = match &args[1] {
        Value::Str(s) => s.as_str(),
        other => panic!(
            "quantity must be plain decimal text, not negative, received {:?}",
            other
        ),
    };
    let year = match &args[3] {
        Value::Int(i) => *i,
        other => panic!("factorYear must be a whole year, received {:?}", other),
    };
    fleet_emissions_to_value(&fleet_emissions(args[0].as_str(), quantity, args[2].as_str(), year))
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and nothing else, pins them in fune.lock, downloads only the Rust 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. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:

fune add fleet.emissions
Download for Rust fleet.emissions-1.0.1-rust.fune · 136,933 bytes sha256 c3896409dcc4affebc9e3dbb5ed9e270af2bd4fc3acfb0d5672f7f6c68f04439

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

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