Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { type RoundingMode } from "./math_round_div.ts"; ← from math.round-div ^1.0.0 · built alongside by fune
import { roundDivBig } from "./math_round_div_big.ts"; ← from math.round-div-big ^1.0.0 · built alongside by fune
import { type Money, money } from "./money_amount.ts"; ← from money.amount ^1.0.0 · built alongside by fune
import { convertUnits } from "./units_convert.ts"; ← from units.convert ^1.0.0 · built alongside by fune
import { type DistanceUnit, type EconomyUnit, type FuelCost } from "./fleet_fuel_cost_types.ts";
const DECIMAL = /^[0-9]+(\.[0-9]+)?$/;
const MAX_SAFE = 9007199254740991n;
/** A plain decimal as an exact fraction: [digits without the point, power of ten]. */
function decimal(name: string, text: string): [bigint, bigint] {
if (typeof text !== "string" || !DECIMAL.test(text)) {
throw new RangeError(`${name} must be a plain decimal of 0 or more, received "${text}"`);
}
const point = text.indexOf(".");
const places = point < 0 ? 0 : text.length - point - 1;
return [BigInt(text.replace(".", "")), 10n ** BigInt(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 (4.54609 L, 3.785411784 L,
* 1.609344 km).
*/
export function fuelCost(
distance: string,
distanceUnit: DistanceUnit,
economy: string,
economyUnit: EconomyUnit,
pricePerLitreTenths: number,
currency: string,
mode: RoundingMode
): FuelCost {
if (distanceUnit !== "mi" && distanceUnit !== "km") {
throw new RangeError(`unknown distance unit "${distanceUnit}"`);
}
if (economyUnit !== "mpg-uk" && economyUnit !== "mpg-us" && economyUnit !== "l-per-100km") {
throw new RangeError(`unknown economy unit "${economyUnit}"`);
}
const [dNum, dDen] = decimal("distance", distance);
const [eNum, eDen] = decimal("economy", economy);
if (eNum === 0n) {
throw new RangeError("economy must be greater than zero");
}
if (!Number.isInteger(pricePerLitreTenths)) {
throw new TypeError(`pricePerLitreTenths must be an integer, received ${pricePerLitreTenths}`);
}
if (pricePerLitreTenths < 0) {
throw new RangeError(`pricePerLitreTenths must not be negative, received ${pricePerLitreTenths}`);
}
money(0, currency);
const [kmNum, kmDen] = decimal("mile", convertUnits("1", "mi", "km", 12));
let lNum: bigint;
let lDen: bigint;
if (economyUnit === "l-per-100km") {
// litres = km x (L/100km) / 100
lNum = dNum * eNum * (distanceUnit === "mi" ? kmNum : 1n);
lDen = dDen * eDen * 100n * (distanceUnit === "mi" ? kmDen : 1n);
} else {
// litres = miles / mpg x litres per gallon
const [gNum, gDen] = decimal("gallon", convertUnits("1", economyUnit === "mpg-uk" ? "gal_imp" : "gal_us", "L", 12));
lNum = dNum * eDen * gNum * (distanceUnit === "km" ? kmDen : 1n);
lDen = dDen * eNum * gDen * (distanceUnit === "km" ? kmNum : 1n);
}
const cost = BigInt(roundDivBig((lNum * BigInt(pricePerLitreTenths)).toString(), (lDen * 10n).toString(), mode));
if (cost > MAX_SAFE) {
throw new RangeError("the fuel cost exceeds 2^53 - 1 minor units");
}
const thousandths = BigInt(roundDivBig((lNum * 1000n).toString(), lDen.toString(), "half-up"));
const whole = (thousandths / 1000n).toString();
const frac = (thousandths % 1000n).toString().padStart(3, "0");
const result: Money = money(Number(cost), currency);
return { litres: `${whole}.${frac}`, cost: result };
}