Functional Weave
Code in Rust

fleet.vehicle-depreciation@1.0.0

impl/typescript.ts

2,719 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 { type VehicleValue } from "./fleet_vehicle_depreciation_types.ts";

function whole(name: string, value: number, max: number): void {
  if (!Number.isInteger(value) || value < 0 || value > max) {
    throw new RangeError(`${name} must be a whole number from 0 to ${max}, received ${value}`);
  }
}

/**
 * A vehicle's value from its age and mileage, on the caller's retention curves.
 *
 * Each whole year multiplies by that year's retention and a part year by a
 * straight-line share of it; each whole 10,000 miles multiplies by the mileage
 * retention and the remaining miles by a straight-line share. The product is
 * kept as one exact fraction and rounded once: rounding the value year by year
 * drifts by a penny or more over a long life.
 */
export function vehicleDepreciation(
  cost: Money,
  ageMonths: number,
  mileage: number,
  retainedPerYearBasisPoints: readonly number[],
  retainedPer10kMilesBasisPoints: number,
  mode: RoundingMode
): VehicleValue {
  if (cost.minor < 0) {
    throw new RangeError(`cost must not be negative, received ${cost.minor}`);
  }
  whole("ageMonths", ageMonths, 600);
  whole("mileage", mileage, 2000000);
  if (retainedPerYearBasisPoints.length === 0) {
    throw new RangeError("retainedPerYearBasisPoints must have at least one year");
  }
  for (const bp of retainedPerYearBasisPoints) whole("a retention rate", bp, 10000);
  whole("a retention rate", retainedPer10kMilesBasisPoints, 10000);

  let num = 1n;
  let den = 1n;
  const years = Math.floor(ageMonths / 12);
  const last = retainedPerYearBasisPoints.length - 1;
  const rateFor = (year: number): bigint => BigInt(retainedPerYearBasisPoints[Math.min(year, last)]);
  for (let year = 0; year < years; year++) {
    num *= rateFor(year);
    den *= 10000n;
  }
  const months = BigInt(ageMonths % 12);
  if (months > 0n) {
    num *= 120000n - (10000n - rateFor(years)) * months;
    den *= 120000n;
  }
  const mileRate = BigInt(retainedPer10kMilesBasisPoints);
  for (let band = 0; band < Math.floor(mileage / 10000); band++) {
    num *= mileRate;
    den *= 10000n;
  }
  const miles = BigInt(mileage % 10000);
  if (miles > 0n) {
    num *= 100000000n - (10000n - mileRate) * miles;
    den *= 100000000n;
  }

  const value = Number(roundDivBig((BigInt(cost.minor) * num).toString(), den.toString(), mode));
  return {
    value: money(value, cost.currency),
    depreciation: money(cost.minor - value, cost.currency),
    retainedBasisPoints: Number(roundDivBig((num * 10000n).toString(), den.toString(), "half-up")),
  };
}