fleet.fuel-cost
Fuel cost of a trip from distance, fuel economy (UK or US mpg, or L/100km) and price per litre, exactly.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 24 tests, run in TypeScript, Python and Rust.
What it does
The fuel a trip burns and what it costs, from the distance, the vehicle's fuel economy and the pump price per litre. `fuelCost("100", "mi", "50", "mpg-uk", 1459, "GBP", "half-up")` is 9.092 litres costing 13.27.
## Inputs
For example
fuel_cost(100, mi, 50, mpg-uk, 1,459, GBP, half-up)→ litres 9.092, cost £13.27 100 miles at 50 mpg (UK) and 145.9p: 9.09218 litres, 1326.549pfuel_cost(100, km, 5.5, l-per-100km, 1,459, GBP, half-up)→ litres 5.500, cost £8.02 100 km at 5.5 L/100km and 145.9p: 5.5 litres, 802.45p rounds down under half-upfuel_cost(100, km, 5.5, l-per-100km, 1,459, GBP, up)→ litres 5.500, cost £8.03 the same trip rounded up
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 fuel_cost(distance: &str, distance_unit: &str, economy: &str, economy_unit: &str, price_per_litre_tenths: i64, currency: &str, mode: &str) -> FuelCost
| distance | string | plain decimal text, 0 or more: "250", "12.5" |
| distance_unit | DistanceUnit | mi or km |
| economy | string | plain decimal text, above 0: "48.7" mpg or "5.9" L/100km |
| economy_unit | EconomyUnit | mpg-uk (imperial gallon), mpg-us (US gallon) or l-per-100km |
| price_per_litre_tenths | int | pump price in tenths of a minor unit, as pumps show it: 1459 = 145.9p |
| currency | string | ISO 4217 code of the price |
| mode | RoundingMode | how the exact cost is rounded to a minor unit |
| returns | FuelCost |
The types it declares, generated into your project
// DistanceUnit is a string in Rust, one of: "mi", "km".
// Parameters take it as &str and results hold it as String.
// EconomyUnit is a string in Rust, one of: "mpg-uk", "mpg-us", "l-per-100km".
// Parameters take it as &str and results hold it as String.
/// The fuel a trip burns and what it costs.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FuelCost {
/// exact litres rounded half-up to 3 decimal places, for display: "9.092"
pub litres: String,
/// from the exact litres, not the rounded ones
pub cost: Money,
}
Your code names it in one line, in the file that uses it
fune!(fleet.fuel-cost@^1); // then call fuel_cost(…)
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::math_big_integer::{parse_big_integer, BigInt}; ← from math.big-integer ^1.0.0 · built alongside by fune
use super::math_round_div_big::round_div_big; ← from math.round-div-big ^1.0.0 · built alongside by fune
use super::money_amount::{money, money_to_value}; ← from money.amount ^1.0.0 · built alongside by fune
use super::units_convert::convert_units; ← from units.convert ^1.0.0 · built alongside by fune
const MAX_SAFE: i128 = 9007199254740991;
/// A plain decimal as an exact fraction: (digits without the point, power of ten).
fn decimal(name: &str, text: &str) -> (BigInt, BigInt) {
let mut parts = text.splitn(2, '.');
let whole = parts.next().unwrap_or("");
let frac = parts.next();
let ok = !whole.is_empty()
&& whole.bytes().all(|b| b.is_ascii_digit())
&& frac.map_or(true, |f| !f.is_empty() && f.bytes().all(|b| b.is_ascii_digit()));
if !ok {
panic!("{} must be a plain decimal of 0 or more, received \"{}\"", name, text);
}
let digits: String = text.chars().filter(|c| *c != '.').collect();
let trimmed = digits.trim_start_matches('0');
let value = parse_big_integer(if trimmed.is_empty() { "0" } else { trimmed });
let places = frac.map_or(0, |f| f.len()) as u64;
(value, BigInt::from_i64(10).pow(places))
}
/// The fuel a trip uses and what it costs.
///
/// Everything stays an exact fraction until the one rounding step: litres come
/// from the distance and economy, and the cost from those exact litres.
/// Rounding the litres first, or using 4.546 for the imperial gallon, is a
/// penny out often enough to break a reconciliation. The gallon and the mile
/// come from units.convert, which defines them exactly.
///
/// # Panics
/// Panics on an unknown unit, a malformed or negative number, a zero economy,
/// a negative price, a bad currency or mode, or a cost beyond 2^53 - 1.
pub fn fuel_cost(
distance: &str,
distance_unit: &str,
economy: &str,
economy_unit: &str,
price_per_litre_tenths: i64,
currency: &str,
mode: &str,
) -> FuelCost {
if distance_unit != "mi" && distance_unit != "km" {
panic!("unknown distance unit \"{}\"", distance_unit);
}
if economy_unit != "mpg-uk" && economy_unit != "mpg-us" && economy_unit != "l-per-100km" {
panic!("unknown economy unit \"{}\"", economy_unit);
}
let (d_num, d_den) = decimal("distance", distance);
let (e_num, e_den) = decimal("economy", economy);
if e_num.is_zero() {
panic!("economy must be greater than zero");
}
if price_per_litre_tenths < 0 {
panic!("pricePerLitreTenths must not be negative, received {}", price_per_litre_tenths);
}
money(0, currency);
let one = BigInt::from_i64(1);
let (km_num, km_den) = decimal("mile", &convert_units("1", "mi", "km", 12));
let (l_num, l_den) = if economy_unit == "l-per-100km" {
// litres = km x (L/100km) / 100
let (fn_, fd) = if distance_unit == "mi" { (km_num, km_den) } else { (one.clone(), one.clone()) };
(d_num.mul(&e_num).mul(&fn_), d_den.mul(&e_den).mul(&BigInt::from_i64(100)).mul(&fd))
} else {
// litres = miles / mpg x litres per gallon
let gallon = if economy_unit == "mpg-uk" { "gal_imp" } else { "gal_us" };
let (g_num, g_den) = decimal("gallon", &convert_units("1", gallon, "L", 12));
let (fn_, fd) = if distance_unit == "km" { (km_den, km_num) } else { (one.clone(), one.clone()) };
(d_num.mul(&e_den).mul(&g_num).mul(&fn_), d_den.mul(&e_num).mul(&g_den).mul(&fd))
};
let cost_text = round_div_big(
&l_num.mul(&BigInt::from_i64(price_per_litre_tenths)).to_string(),
&l_den.mul(&BigInt::from_i64(10)).to_string(),
mode,
);
let cost = parse_big_integer(&cost_text);
if cost > BigInt::from_i128(MAX_SAFE) {
panic!("the fuel cost exceeds 2^53 - 1 minor units");
}
let thousandths = parse_big_integer(&round_div_big(
&l_num.mul(&BigInt::from_i64(1000)).to_string(),
&l_den.to_string(),
"half-up",
));
let (whole, frac) = thousandths.div_rem(&BigInt::from_i64(1000));
FuelCost {
litres: format!("{}.{:03}", whole, frac.to_i64()),
cost: money(cost.to_i64(), currency),
}
}
pub fn fuel_cost_to_value(f: &FuelCost) -> Value {
Value::obj(vec![("litres", Value::str(&f.litres)), ("cost", money_to_value(&f.cost))])
}
pub fn fune_vector(args: &[Value]) -> Value {
if let Value::Float(f) = &args[4] {
panic!("pricePerLitreTenths must be an integer, received {}", f);
}
for (i, name) in [(0usize, "distance"), (2usize, "economy")] {
if !matches!(args[i], Value::Str(_)) {
panic!("{} must be a plain decimal of 0 or more, received a non-string", name);
}
}
fuel_cost_to_value(&fuel_cost(
args[0].as_str(),
args[1].as_str(),
args[2].as_str(),
args[3].as_str(),
args[4].as_i64(),
args[5].as_str(),
args[6].as_str(),
))
}Install
fune build
With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 5 dependencies, 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.fuel-cost
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./fleet.fuel-cost-1.0.0-rust.fune, or fetch it from a terminal with fune pull fleet.fuel-cost@1.0.0:rust.
The whole function, every language, is one file too: fleet.fuel-cost-1.0.0.fune, 22,193 bytes, sha256 5022019ff35116bcc4305837d21b577312e7a9eb682e807d301f46e7f00b5dc3. 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.fuel-cost
after — your function gets the result and the arguments, and returns the final result.
// fune: after fleet.fuel-cost
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 math.big-integer in fleet.fuel-cost
// fune: replace math.round-div in fleet.fuel-cost
// fune: replace math.round-div-big in fleet.fuel-cost
// fune: replace money.amount in fleet.fuel-cost
// fune: replace units.convert in fleet.fuel-cost
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.fuel-cost --steps.
// fune: step fleet.fuel-cost 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 | |
|---|---|---|---|
| 100 miles at 50 mpg (UK) and 145.9p: 9.09218 litres, 1326.549p | 100, mi, 50, mpg-uk, 1,459, GBP, half-up | → | litres 9.092, cost £13.27 |
| 100 km at 5.5 L/100km and 145.9p: 5.5 litres, 802.45p rounds down under half-up | 100, km, 5.5, l-per-100km, 1,459, GBP, half-up | → | litres 5.500, cost £8.02 |
| the same trip rounded up | 100, km, 5.5, l-per-100km, 1,459, GBP, up | → | litres 5.500, cost £8.03 |
| 100 miles at 5.5 L/100km: 160.9344 km, 8.851392 litres | 100, mi, 5.5, l-per-100km, 1,459, GBP, half-up | → | litres 8.851, cost £12.91 |
| 100 km at 50 mpg (UK): 454.609 / 80.4672 = 5.6496... litres | 100, km, 50, mpg-uk, 1,459, GBP, half-up | → | litres 5.650, cost £8.24 |
| 100 miles at 30 mpg (US): the US gallon is 3.785411784 litres | 100, mi, 30, mpg-us, 1,459, GBP, half-up | → | litres 12.618, cost £18.41 |
| one mile at 1 mpg (UK) and 81.5p is 370.506335p: rounding to 4.546 litres first would give 370 | 1, mi, 1, mpg-uk, 815, GBP, half-up | → | litres 4.546, cost £3.71 |
| an exact half penny: 1 litre at 100.5p, half-up | 100, km, 1, l-per-100km, 1,005, GBP, half-up | → | litres 1.000, cost £1.01 |
| an exact half penny, half-even keeps the even 100 | 100, km, 1, l-per-100km, 1,005, GBP, half-even | → | litres 1.000, cost £1.00 |
| an exact half penny, down | 100, km, 1, l-per-100km, 1,005, GBP, down | → | litres 1.000, cost £1.00 |
Show the other 14 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| no distance costs nothing | 0, mi, 45, mpg-uk, 1,459, GBP, half-up | → | litres 0.000, cost £0.00 |
| free fuel costs nothing | 250, mi, 45, mpg-uk, 0, GBP, half-up | → | litres 25.256, cost £0.00 |
| euro cents work the same way: 12.5 km at 6.4 L/100km and 1.799 EUR a litre | 12.5, km, 6.4, l-per-100km, 1,799, EUR, half-up | → | litres 0.800, cost €1.44 |
| an economy of zero is an error | 100, mi, 0, mpg-uk, 1,459, GBP, half-up | → | error: economy must be greater than zero |
| a negative distance is an error | -5, mi, 50, mpg-uk, 1,459, GBP, half-up | → | error: distance must be a plain decimal of 0 or more |
| an exponent is not a plain decimal | 1e3, km, 50, mpg-uk, 1,459, GBP, half-up | → | error: distance must be a plain decimal of 0 or more |
| a trailing point is not a plain decimal | 50, km, 6., l-per-100km, 1,459, GBP, half-up | → | error: economy must be a plain decimal of 0 or more |
| an unknown distance unit is an error | 100, yd, 50, mpg-uk, 1,459, GBP, half-up | → | error: unknown distance unit |
| mpg without saying which gallon is an error | 100, mi, 50, mpg, 1,459, GBP, half-up | → | error: unknown economy unit |
| a negative price is an error | 100, mi, 50, mpg-uk, -1, GBP, half-up | → | error: pricePerLitreTenths must not be negative |
| a fractional price must be given in tenths | 100, mi, 50, mpg-uk, 145.9, GBP, half-up | → | error: pricePerLitreTenths must be an integer |
| a lower-case currency is an error | 100, mi, 50, mpg-uk, 1,459, gbp, half-up | → | error: is not an uppercase ISO 4217 currency code |
| an unknown rounding mode is an error | 100, mi, 50, mpg-uk, 1,459, GBP, nearest | → | error: unknown rounding mode |
| a cost beyond 2^53 - 1 minor units is refused | 999999999999999999, mi, 1, mpg-uk, 99,999, GBP, half-up | → | error: the fuel cost exceeds 2^53 - 1 minor units |
More from the author
- `distance` and `economy` are plain decimal text ("250", "48.7"), so a distance read off an odometer or a WLTP figure goes in exactly. - `economyUnit` names the gallon. UK figures are miles per **imperial** gallon (4.54609 L); US figures are miles per US gallon (3.785411784 L), about 20% smaller. Plain `mpg` is refused because the two are easy to mix up. Or give litres per 100 km. - `pricePerLitreTenths` is the price as a pump shows it, to a tenth of a penny: 145.9p is `1459`. A price in whole pence is just `1450` and so on.
## Exactness
The litres are an exact fraction of the inputs. The mile (1.609344 km) and both gallons come from `units.convert`, which defines them exactly. The cost is litres x price, rounded once to a minor unit with the mode you choose. The litres in the result are rounded half-up to 3 places for display only: the cost never uses them. That matters: one mile at 1 mpg (UK) and 81.5p is 370.506335p, which is 371p; rounding the litres to 4.546 first (or using the common approximation 4.546 L per gallon) gives 370p.
The product is held in big integers (`math.round-div-big`), so long distances with many decimal places cannot overflow; a cost beyond 2^53 - 1 minor units is refused because JavaScript could not represent it.
## Edges
Zero distance and a zero price cost nothing. A zero economy, a negative or malformed number, an unknown unit, a fractional or negative price, a bad currency code and an unknown mode are errors.
Files
| Path | Bytes |
|---|---|
| README.md | 1,722 |
| impl/python.py | 3,263 |
| impl/rust.rs | 4,851 |
| impl/typescript.ts | 3,523 |
| vectors.json | 4,570 |