How the Decision tools car total cost of ownership calculator is verified
Inputs
| Input | Field | Type |
|---|---|---|
| Purchase price | purchasePrice | number |
| Deposit | deposit | number |
| Trade-in value | tradeIn | number |
| Finance APR (%) | aprPct | number |
| Finance term (months) | loanTermMonths | integer |
| Years you keep it | years | integer |
| Distance per year (km) | annualDistance | number |
| Fuel type | fuelType | string |
| Consumption per 100 km (L, or kWh for an EV) | consumptionPer100 | number |
| Price per litre (or per kWh) | unitPrice | number |
| Insurance per year | insurancePerYear | number |
| Servicing per year | servicingPerYear | number |
| Repairs allowance per year | repairsPerYear | number |
| Cost of a set of tyres | tyreSetCost | number |
| Tyre life (km) | tyreLifeKm | number |
| Road tax per year | roadTaxPerYear | number |
| Inspection per year | inspectionPerYear | number |
| Parking per year | parkingPerYear | number |
| Tolls per year | tollsPerYear | number |
| Breakdown cover per year | breakdownCoverPerYear | number |
The formula
Each derived value below is computed by the Valem engine from the expression shown, in this order. Nothing else runs — there is no hidden code path.
| Derived value | Field | Formula (JSONata) |
|---|---|---|
| Financed Amount | financedAmount | $max([0, $round(purchasePrice - deposit - tradeIn, 2)]) |
| Monthly Rate | monthlyRate | aprPct / 1200 |
| Compound Factor | compoundFactor | $power(1 + monthlyRate, loanTermMonths) |
| Monthly finance payment | monthlyPayment | (financedAmount <= 0 or loanTermMonths <= 0) ? 0 : (monthlyRate = 0 ? $round(financedAmount / loanTermMonths, 2) : $round(financedAmount * monthlyRate * compoundFactor / (compoundFactor - 1), 2)) |
| Finance interest | totalInterest | $max([0, $round(monthlyPayment * loanTermMonths - financedAmount, 2)]) |
| Resale value at the end | residualValue | $round(purchasePrice * $residualPct(years, $const.residualCurve, $const.tailDepreciation), 2) |
| Depreciation | totalDepreciation | $round(purchasePrice - residualValue, 2) |
| Energy Cost Per Year | energyCostPerYear | $round(annualDistance / 100 * consumptionPer100 * unitPrice, 2) |
| Tyre Cost Per Year | tyreCostPerYear | $round($ceil(annualDistance * years / tyreLifeKm) * tyreSetCost / years, 2) |
| Fixed Cost Per Year | fixedCostPerYear | $round(insurancePerYear + servicingPerYear + repairsPerYear + roadTaxPerYear + inspectionPerYear + parkingPerYear + tollsPerYear + breakdownCoverPerYear, 2) |
| What you pay each year, by cost category | yearly | $map([1..years], function($y) { ( $m1 := $min([($y - 1) * 12, loanTermMonths]); $m2 := $min([$y * 12, loanTermMonths]); $b1 := $balanceAt($m1, financedAmount, monthlyRate, compoundFactor, loanTermMonths); $b2 := $balanceAt($m2, financedAmount, monthlyRate, compoundFactor, loanTermMonths); $int := $max([0, $round(monthlyPayment * ($m2 - $m1) - ($b1 - $b2), 2)]); $v0 := $round(purchasePrice * $residualPct($y - 1, $const.residualCurve, $const.tailDepreciation), 2); $v1 := $round(purchasePrice * $residualPct($y, $const.residualCurve, $const.tailDepreciation), 2); $dep := $round($v0 - $v1, 2); $cumInt := $max([0, $round(monthlyPayment * $m2 - (financedAmount - $b2), 2)]); {"year": $y, "depreciation": $dep, "interest": $int, "energy": energyCostPerYear, "tyres": tyreCostPerYear, "fixed": fixedCostPerYear, "total": $round($dep + $int + energyCostPerYear + tyreCostPerYear + fixedCostPerYear, 2), "cumulative": $round(purchasePrice - $v1 + $cumInt + $y * (energyCostPerYear + tyreCostPerYear + fixedCostPerYear), 2)} ) }) |
| Total cost of ownership | totalCost | $round($sum($map(yearly, function($r) { $r.total })), 2) |
| Cost per km | costPerKm | annualDistance <= 0 ? 0 : $round(totalCost / (annualDistance * years), 4) |
| Cost per month | costPerMonth | $round(totalCost / (years * 12), 2) |
| Depreciation share | depreciationShare | totalCost <= 0 ? 0 : $round(totalDepreciation / totalCost * 100, 1) |
Rates and thresholds
The expressions above read these as $const. They are the figures the annual refresh replaces, so they live as data rather than being written into the formulas — which is what makes a year-on-year change a one-line diff instead of an edit to arithmetic.
| Constant | Value |
|---|---|
residualCurve | [0.8,0.7,0.62,0.55,0.49,0.44,0.39,0.35,0.31,0.28] |
tailDepreciation | 0.1 |
Test vectors (3)
Every case is executed against this model on each build, by the Java engine and independently by a JavaScript one. A mismatch of a single cent fails the build, so the page cannot ship advertising a figure the model no longer produces.
Case 1 — 32,000 petrol car on 6.5% finance over 60 months, 15,000 km a year, kept five years
given purchasePrice | 32000 |
|---|---|
given deposit | 6000 |
given aprPct | 6.5 |
given loanTermMonths | 60 |
given years | 5 |
given annualDistance | 15000 |
expect financedAmount | 26000 |
expect monthlyPayment | 508.72 |
expect totalInterest | 4523.2 |
expect residualValue | 15680 |
expect totalDepreciation | 16320 |
expect energyCostPerYear | 1706.25 |
expect tyreCostPerYear | 192 |
expect fixedCostPerYear | 1960 |
expect yearly[0].depreciation | 6400 |
expect yearly[0].interest | 1556.08 |
expect yearly[0].total | 11814.33 |
expect yearly[4].cumulative | 40134.45 |
expect totalCost | 40134.45 |
expect costPerKm | 0.5351 |
expect costPerMonth | 668.91 |
expect depreciationShare | 40.7 |
Case 2 — the same car bought for cash — the total falls by exactly the finance interest
given deposit | 32000 |
|---|---|
expect financedAmount | 0 |
expect monthlyPayment | 0 |
expect totalInterest | 0 |
expect totalCost | 35611.25 |
expect costPerKm | 0.4748 |
expect costPerMonth | 593.52 |
Case 3 — the same car as an EV at 17 kWh/100 km and 0.28 per kWh — only the energy line changes
given fuelType | ev |
|---|---|
given consumptionPer100 | 17 |
given unitPrice | 0.28 |
expect energyCostPerYear | 714 |
expect totalInterest | 4523.2 |
expect totalDepreciation | 16320 |
expect totalCost | 35173.2 |
expect costPerKm | 0.469 |
expect depreciationShare | 46.4 |
Sources
Check it yourself — and reuse it
The whole model as JSON — inputs, outputs, formulas, vectors and sources in one re-runnable document.
This model is published under CC BY 4.0: use it, adapt it, build on it, with attribution. The licence covers the model — the expression of each rule and the arrangement of the catalog. The rates and thresholds are law, and nobody licenses those; the sources above are where they come from.
← Back to the Decision tools car total cost of ownership calculator
Built with Valem — a live, verifiable calculator you can fork and embed.