fleet.cost-per-mile
Whole-life cost per mile of a vehicle, in tenths of a penny, from depreciation, fuel, maintenance, insurance and tax.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 13 tests, run in TypeScript, Python and Rust.
What it does
Whole-life cost per mile of a vehicle: what it cost over a period (depreciation, fuel, maintenance, insurance and tax) divided by the miles it covered, in tenths of a penny, which is how fleet reports quote it ("50.3p a mile").
## Shape
For example
costPerMile(depreciation £3,000.00, fuel £1,800.00, maintenance £600.00, insurance £450.00, tax £190.00, 12,000)→ total cost £6,040.00, depreciation tenths 250, fuel tenths 150, maintenance tenths 50, insurance tenths 38, tax tenths 16, total tenths 503, per mile 50.3 a year's costs over 12,000 miles: 50.3p a mile, though the rounded parts add up to 50.4costPerMile(depreciation £1.00, fuel £2.00, maintenance £3.00, insurance £4.00, tax £5.00, 1)→ total cost £15.00, depreciation tenths 1,000, fuel tenths 2,000, maintenance tenths 3,000, insurance tenths 4,000, tax tenths 5,000, total tenths 15,000, per mile 1500.0 one mile: every penny is ten tenthscostPerMile(depreciation £0.00, fuel £0.00, maintenance £0.00, insurance £0.00, tax £0.00, 5,000)→ total cost £0.00, depreciation tenths 0, fuel tenths 0, maintenance tenths 0, insurance tenths 0, tax tenths 0, total tenths 0, per mile 0.0 nothing spent is 0.0p a mile
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 costPerMile(costs: WholeLifeCosts, miles: number): CostPerMile
| costs | WholeLifeCosts | what the vehicle cost over the period, all in one currency |
| miles | int | miles driven over the same period, above 0 |
| returns | CostPerMile |
The types it declares, generated into your project
/** A vehicle's costs over one period, split the way fleet reports split them. */
export interface WholeLifeCosts {
/** purchase price less resale value; negative if it gained value */
readonly depreciation: Money;
/** fuel or electricity */
readonly fuel: Money;
/** servicing, tyres and repairs */
readonly maintenance: Money;
readonly insurance: Money;
/** vehicle excise duty and similar */
readonly tax: Money;
}
/** Cost per mile in tenths of a minor unit (503 = 50.3p), each rounded half-up on its own. */
export interface CostPerMile {
readonly totalCost: Money;
readonly depreciationTenths: number;
readonly fuelTenths: number;
readonly maintenanceTenths: number;
readonly insuranceTenths: number;
readonly taxTenths: number;
/** from the total cost, so not always the sum of the rounded parts */
readonly totalTenths: number;
/** totalTenths as text with one decimal place: "50.3" */
readonly perMile: string;
}
Your code names it in one line, in the file that uses it
import { costPerMile } from "#fune/fleet.cost-per-mile@^1";
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { roundDiv } from "./math_round_div.ts"; ← from math.round-div ^1.0.0 · built alongside by fune
import { type Money } from "./money_amount.ts"; ← from money.amount ^1.0.0 · built alongside by fune
import { sumMoney } from "./money_sum.ts"; ← from money.sum ^1.0.0 · built alongside by fune
import { type CostPerMile, type WholeLifeCosts } from "./fleet_cost_per_mile_types.ts";
function tenths(amount: Money, miles: number): number {
return roundDiv(amount.minor * 10, miles, "half-up");
}
function display(value: number): string {
const sign = value < 0 ? "-" : "";
const magnitude = Math.abs(value);
return `${sign}${Math.floor(magnitude / 10)}.${magnitude % 10}`;
}
/**
* Whole-life cost per mile, in tenths of a penny, the unit fleet reports use.
*
* Each part is rounded on its own for the breakdown, and the total is rounded
* from the total cost: adding up the rounded parts drifts by a tenth or two,
* which is why the total is not their sum.
*/
export function costPerMile(costs: WholeLifeCosts, miles: number): CostPerMile {
if (!Number.isInteger(miles) || miles <= 0) {
throw new RangeError(`miles must be a whole number above zero, received ${miles}`);
}
const totalCost = sumMoney(
[costs.depreciation, costs.fuel, costs.maintenance, costs.insurance, costs.tax],
costs.depreciation.currency
);
const totalTenths = tenths(totalCost, miles);
return {
totalCost,
depreciationTenths: tenths(costs.depreciation, miles),
fuelTenths: tenths(costs.fuel, miles),
maintenanceTenths: tenths(costs.maintenance, miles),
insuranceTenths: tenths(costs.insurance, miles),
taxTenths: tenths(costs.tax, miles),
totalTenths,
perMile: display(totalTenths),
};
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 3 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 fleet.cost-per-mile
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./fleet.cost-per-mile-1.0.0-typescript.fune, or fetch it from a terminal with fune pull fleet.cost-per-mile@1.0.0:typescript.
The whole function, every language, is one file too: fleet.cost-per-mile-1.0.0.fune, 17,753 bytes, sha256 eac59b91ed735bbe4b2d8f38c349e2f4b840cd26eeecb644a17f65ca12d6f05c. 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.cost-per-mile
after — your function gets the result and the arguments, and returns the final result.
// fune: after fleet.cost-per-mile
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.round-div in fleet.cost-per-mile
// fune: replace money.amount in fleet.cost-per-mile
// fune: replace money.sum in fleet.cost-per-mile
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.cost-per-mile --steps.
// fune: step fleet.cost-per-mile 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 year's costs over 12,000 miles: 50.3p a mile, though the rounded parts add up to 50.4 | depreciation £3,000.00, fuel £1,800.00, maintenance £600.00, insurance £450.00, tax £190.00, 12,000 | → | total cost £6,040.00, depreciation tenths 250, fuel tenths 150, maintenance tenths 50, insurance tenths 38, tax tenths 16, total tenths 503, per mile 50.3 |
| one mile: every penny is ten tenths | depreciation £1.00, fuel £2.00, maintenance £3.00, insurance £4.00, tax £5.00, 1 | → | total cost £15.00, depreciation tenths 1,000, fuel tenths 2,000, maintenance tenths 3,000, insurance tenths 4,000, tax tenths 5,000, total tenths 15,000, per mile 1500.0 |
| nothing spent is 0.0p a mile | depreciation £0.00, fuel £0.00, maintenance £0.00, insurance £0.00, tax £0.00, 5,000 | → | total cost £0.00, depreciation tenths 0, fuel tenths 0, maintenance tenths 0, insurance tenths 0, tax tenths 0, total tenths 0, per mile 0.0 |
| a car that gained value has negative depreciation | depreciation -£120.00, fuel £0.00, maintenance £0.00, insurance £0.00, tax £0.00, 10,000 | → | total cost -£120.00, depreciation tenths -12, fuel tenths 0, maintenance tenths 0, insurance tenths 0, tax tenths 0, total tenths -12, per mile -1.2 |
| half a tenth rounds up | depreciation £0.05, fuel £0.00, maintenance £0.00, insurance £0.00, tax £0.00, 100 | → | total cost £0.05, depreciation tenths 1, fuel tenths 0, maintenance tenths 0, insurance tenths 0, tax tenths 0, total tenths 1, per mile 0.1 |
| a negative half a tenth rounds away from zero | depreciation -£0.05, fuel £0.00, maintenance £0.00, insurance £0.00, tax £0.00, 100 | → | total cost -£0.05, depreciation tenths -1, fuel tenths 0, maintenance tenths 0, insurance tenths 0, tax tenths 0, total tenths -1, per mile -0.1 |
| just under half a tenth rounds down | depreciation £0.00, fuel £0.49, maintenance £0.00, insurance £0.00, tax £0.00, 1,000 | → | total cost £0.49, depreciation tenths 0, fuel tenths 0, maintenance tenths 0, insurance tenths 0, tax tenths 0, total tenths 0, per mile 0.0 |
| a van over 30,000 miles in euro cents | depreciation €9,000.00, fuel €13,500.00, maintenance €2,100.00, insurance €1,200.00, tax €300.00, 30,000 | → | total cost €26,100.00, depreciation tenths 300, fuel tenths 450, maintenance tenths 70, insurance tenths 40, tax tenths 10, total tenths 870, per mile 87.0 |
| three-mile trip: 1 penny of tax is 3.3 tenths, rounded to 3 | depreciation £0.00, fuel £0.00, maintenance £0.00, insurance £0.00, tax £0.01, 3 | → | total cost £0.01, depreciation tenths 0, fuel tenths 0, maintenance tenths 0, insurance tenths 0, tax tenths 3, total tenths 3, per mile 0.3 |
| zero miles is an error | depreciation £1.00, fuel £0.00, maintenance £0.00, insurance £0.00, tax £0.00, 0 | → | error: miles must be a whole number above zero |
Show the other 3 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| negative miles is an error | depreciation £1.00, fuel £0.00, maintenance £0.00, insurance £0.00, tax £0.00, -10 | → | error: miles must be a whole number above zero |
| fractional miles is an error | depreciation £1.00, fuel £0.00, maintenance £0.00, insurance £0.00, tax £0.00, 10.5 | → | error: miles must be a whole number above zero |
| mixed currencies are refused | depreciation £1.00, fuel €1.00, maintenance £0.00, insurance £0.00, tax £0.00, 100 | → | error: currency mismatch |
More from the author
The costs come in as one `WholeLifeCosts` record of `Money`, all in one currency; mixing currencies is an error. The result gives each part per mile, the total per mile, and the total as text (`"50.3"`), all as integers in tenths of a minor unit, rounded half-up (half a tenth away from zero).
## Rounding
Every figure is rounded on its own, and the total is rounded from the total cost, not added up from the rounded parts. 6,040.00 over 12,000 miles is 50.3p a mile; the rounded parts (25.0 + 15.0 + 5.0 + 3.8 + 1.6) add up to 50.4. A report whose columns must add up should show the parts to more places or footnote the difference.
## Edges
Depreciation can be negative (a vehicle sold for more than it cost), and the per-mile figures are then negative too. Miles must be a whole number above zero. This does not work out the costs themselves: see `fleet.fuel-cost`, `fleet.vehicle-depreciation` and `fleet.ev-charging-cost`.
Files
| Path | Bytes |
|---|---|
| README.md | 1,195 |
| impl/python.py | 1,594 |
| impl/rust.rs | 2,840 |
| impl/typescript.ts | 1,579 |
| vectors.json | 5,985 |