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 };
}