Functional Weave
Code in Rust

math.basis-points@1.0.0

impl/typescript.ts

2,412 bytes · the TypeScript implementation · view raw

import { type RateUnit } from "./math_basis_points_types.ts";

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

/** Powers of ten between each unit and basis points. */
function exponent(unit: RateUnit): number {
  switch (unit) {
    case "basis-points":
      return 0;
    case "percent":
      return 2;
    case "ratio":
      return 4;
    default:
      throw new RangeError(`unknown rate unit "${unit}"`);
  }
}

/**
 * Convert a rate between percent, basis points and a ratio.
 *
 * The units differ by powers of ten, so this moves the decimal point in the
 * text itself: exact for any input, with no float anywhere.
 */
export function convertRate(value: string, fromUnit: RateUnit, toUnit: RateUnit): string {
  if (typeof value !== "string" || !DECIMAL.test(value)) {
    throw new RangeError(`"${value}" is not a decimal number`);
  }
  const shift = exponent(fromUnit) - exponent(toUnit);

  const negative = value.startsWith("-");
  const body = negative ? value.slice(1) : value;
  const point = body.indexOf(".");
  let digits = point < 0 ? body : body.slice(0, point) + body.slice(point + 1);
  let position = (point < 0 ? body.length : point) + shift;

  if (position > digits.length) digits = digits + "0".repeat(position - digits.length);
  if (position < 0) {
    digits = "0".repeat(-position) + digits;
    position = 0;
  }

  const whole = digits.slice(0, position).replace(/^0+/, "") || "0";
  const fraction = digits.slice(position).replace(/0+$/, "");
  const text = fraction ? `${whole}.${fraction}` : whole;
  return negative && text !== "0" ? "-" + text : text;
}

/** An integer number of basis points, refusing a rate that is not whole. */
export function toBasisPoints(value: string, unit: RateUnit): number {
  const text = convertRate(value, unit, "basis-points");
  if (text.includes(".")) {
    throw new RangeError(`${value} ${unit} is not a whole number of basis points`);
  }
  const bp = Number(text);
  if (!Number.isSafeInteger(bp)) {
    throw new RangeError(`${value} ${unit} is too large for integer basis points`);
  }
  return bp;
}

/** Render integer basis points in another unit. */
export function fromBasisPoints(basisPoints: number, unit: RateUnit): string {
  if (!Number.isSafeInteger(basisPoints)) {
    throw new TypeError(`basisPoints must be a safe integer, received ${basisPoints}`);
  }
  return convertRate(String(basisPoints), "basis-points", unit);
}