Functional Weave
Code in Rust

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

  • cost_per_mile(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.4
  • cost_per_mile(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 tenths
  • cost_per_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 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.

pub fn cost_per_mile(costs: &WholeLifeCosts, miles: i64) -> CostPerMile
costsWholeLifeCostswhat the vehicle cost over the period, all in one currency
milesintmiles driven over the same period, above 0
returnsCostPerMile

The types it declares, generated into your project

/// A vehicle's costs over one period, split the way fleet reports split them.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WholeLifeCosts {
    /// purchase price less resale value; negative if it gained value
    pub depreciation: Money,
    /// fuel or electricity
    pub fuel: Money,
    /// servicing, tyres and repairs
    pub maintenance: Money,
    pub insurance: Money,
    /// vehicle excise duty and similar
    pub tax: Money,
}

/// Cost per mile in tenths of a minor unit (503 = 50.3p), each rounded half-up on its own.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CostPerMile {
    pub total_cost: Money,
    pub depreciation_tenths: i64,
    pub fuel_tenths: i64,
    pub maintenance_tenths: i64,
    pub insurance_tenths: i64,
    pub tax_tenths: i64,
    /// from the total cost, so not always the sum of the rounded parts
    pub total_tenths: i64,
    /// totalTenths as text with one decimal place: "50.3"
    pub per_mile: String,
}

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

fune!(fleet.cost-per-mile@^1);  // then call cost_per_mile(…)
impl/rust.rs · 77 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::math_round_div::round_div;  ← from math.round-div ^1.0.0 · built alongside by fune
use super::money_amount::{money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune
use super::money_sum::sum_money;  ← from money.sum ^1.0.0 · built alongside by fune

fn tenths(amount: &Money, miles: i64) -> i64 {
    round_div(amount.minor * 10, miles, "half-up")
}

fn display(value: i64) -> String {
    let sign = if value < 0 { "-" } else { "" };
    let magnitude = value.unsigned_abs();
    format!("{}{}.{}", sign, 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.
///
/// # Panics
/// Panics if miles is not above zero or the costs mix currencies.
pub fn cost_per_mile(costs: &WholeLifeCosts, miles: i64) -> CostPerMile {
    if miles <= 0 {
        panic!("miles must be a whole number above zero, received {}", miles);
    }
    let total_cost = sum_money(
        &[
            costs.depreciation.clone(),
            costs.fuel.clone(),
            costs.maintenance.clone(),
            costs.insurance.clone(),
            costs.tax.clone(),
        ],
        &costs.depreciation.currency,
    );
    let total_tenths = tenths(&total_cost, miles);
    CostPerMile {
        depreciation_tenths: tenths(&costs.depreciation, miles),
        fuel_tenths: tenths(&costs.fuel, miles),
        maintenance_tenths: tenths(&costs.maintenance, miles),
        insurance_tenths: tenths(&costs.insurance, miles),
        tax_tenths: tenths(&costs.tax, miles),
        total_tenths,
        per_mile: display(total_tenths),
        total_cost,
    }
}

pub fn cost_per_mile_to_value(c: &CostPerMile) -> Value {
    Value::obj(vec![
        ("totalCost", money_to_value(&c.total_cost)),
        ("depreciationTenths", Value::Int(c.depreciation_tenths)),
        ("fuelTenths", Value::Int(c.fuel_tenths)),
        ("maintenanceTenths", Value::Int(c.maintenance_tenths)),
        ("insuranceTenths", Value::Int(c.insurance_tenths)),
        ("taxTenths", Value::Int(c.tax_tenths)),
        ("totalTenths", Value::Int(c.total_tenths)),
        ("perMile", Value::str(&c.per_mile)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    if let Value::Float(f) = &args[1] {
        panic!("miles must be a whole number above zero, received {}", f);
    }
    let c = &args[0];
    let costs = WholeLifeCosts {
        depreciation: money_from_value(c.get("depreciation")),
        fuel: money_from_value(c.get("fuel")),
        maintenance: money_from_value(c.get("maintenance")),
        insurance: money_from_value(c.get("insurance")),
        tax: money_from_value(c.get("tax")),
    };
    cost_per_mile_to_value(&cost_per_mile(&costs, args[1].as_i64()))
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 3 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.cost-per-mile
Download for Rust fleet.cost-per-mile-1.0.0-rust.fune · 14,434 bytes sha256 19f49a2d97f0845e5c84ec264b8a8f42221e55a680f6e2ffcead28563e72fda4

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

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.

CaseArgumentsExpected
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
CaseArgumentsExpected
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

PathBytes
README.md1,195
impl/python.py1,594
impl/rust.rs2,840
impl/typescript.ts1,579
vectors.json5,985