Functional Weave
Code in Rust

fleet.fuel-cost@1.0.0

impl/typescript.ts

3,523 bytes · the TypeScript implementation · view raw

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 };
}