Functional Weave
Code in TypeScript

units.convert@1.0.0

impl/typescript.ts

3,758 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 { UNITS, type UnitDefinition } from "./units_convert_data.ts";  ← this capability’s own data, compiled from data/units.json into the same file by fune build

const DECIMAL = /^-?[0-9]+(\.[0-9]+)?$/;
const MAX_DIGITS = 15;

function gcd(a: bigint, b: bigint): bigint {
  let x = a < 0n ? -a : a;
  let y = b < 0n ? -b : b;
  while (y !== 0n) [x, y] = [y, x % y];
  return x;
}

function unit(symbol: string): UnitDefinition {
  const found = UNITS.find((u) => u.symbol === symbol);
  if (!found) throw new RangeError(`unknown unit "${symbol}"`);
  return found;
}

/**
 * Convert a quantity between units of the same dimension, exactly.
 *
 * The value travels as decimal text and every factor is an exact fraction
 * (1 in = 254/10000 m, 1 lb = 45359237/100000000 kg), so the only rounding is
 * the one at the end, to `decimals` places. Multiplying floats instead gets
 * 0.75 in = 19.049999999999997 mm, which rounds to 19.0 rather than 19.1.
 */
export function convertUnits(value: string, fromUnit: string, toUnit: string, decimals: number): string {
  if (typeof value !== "string" || !DECIMAL.test(value)) {
    throw new RangeError(`value must be a plain decimal like "12.5", received "${value}"`);
  }
  const negative = value.startsWith("-");
  const [whole, rawFraction = ""] = (negative ? value.slice(1) : value).split(".");
  const fraction = rawFraction.replace(/0+$/, "");
  const digits = (whole + fraction).replace(/^0+/, "");
  if (digits.length > MAX_DIGITS || fraction.length > MAX_DIGITS) {
    throw new RangeError(`value "${value}" has too many digits: at most 15 significant digits and 15 decimal places`);
  }

  const from = unit(fromUnit);
  const to = unit(toUnit);
  if (from.dimension !== to.dimension) {
    throw new RangeError(`cannot convert ${fromUnit} (${from.dimension}) to ${toUnit} (${to.dimension})`);
  }
  if (!Number.isInteger(decimals) || decimals < 0 || decimals > 12) {
    throw new RangeError(`decimals must be a whole number from 0 to 12, received ${decimals}`);
  }

  // base = v * fa + oa, result = (base - ob) / fb
  //      = v * P/Q + R/S, with P/Q = fa/fb and R/S = (oa - ob)/fb, each reduced.
  let p = BigInt(from.factorNumerator) * BigInt(to.factorDenominator);
  let q = BigInt(from.factorDenominator) * BigInt(to.factorNumerator);
  let g = gcd(p, q);
  p /= g;
  q /= g;
  let r =
    (BigInt(from.offsetNumerator) * BigInt(to.offsetDenominator) - BigInt(to.offsetNumerator) * BigInt(from.offsetDenominator)) *
    BigInt(to.factorDenominator);
  let s = BigInt(from.offsetDenominator) * BigInt(to.offsetDenominator) * BigInt(to.factorNumerator);
  g = gcd(r, s);
  if (g !== 0n) {
    r /= g;
    s /= g;
  }

  const scale = 10n ** BigInt(fraction.length);
  const magnitude = BigInt(digits === "" ? "0" : digits);
  const numerator = (negative ? -magnitude : magnitude) * p * s + r * scale * q;
  const denominator = scale * q * s;

  // Long division one digit at a time keeps every intermediate below 10 x the
  // denominator, which is what lets the Rust port do the same in i128.
  const resultNegative = numerator < 0n;
  const absolute = resultNegative ? -numerator : numerator;
  let intPart = absolute / denominator;
  let remainder = absolute % denominator;
  let fracPart = 0n;
  for (let i = 0; i < decimals; i++) {
    remainder *= 10n;
    fracPart = fracPart * 10n + remainder / denominator;
    remainder %= denominator;
  }
  if (remainder * 2n >= denominator) {
    fracPart += 1n;
    if (fracPart === 10n ** BigInt(decimals)) {
      fracPart = 0n;
      intPart += 1n;
    }
  }

  let text = intPart.toString();
  if (decimals > 0) {
    const shown = fracPart.toString().padStart(decimals, "0").replace(/0+$/, "");
    if (shown !== "") text += "." + shown;
  }
  if (resultNegative && (intPart !== 0n || fracPart !== 0n)) text = "-" + text;
  return text;
}