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":
return0;
case"percent":
return2;
case"ratio":
return4;
default:
thrownew 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. */exportfunction convertRate(value: string, fromUnit: RateUnit, toUnit: RateUnit): string {
if (typeof value !== "string" || !DECIMAL.test(value)) {
thrownew 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. */exportfunction toBasisPoints(value: string, unit: RateUnit): number {
const text = convertRate(value, unit, "basis-points");
if (text.includes(".")) {
thrownew RangeError(`${value} ${unit} is not a whole number of basis points`);
}
const bp = Number(text);
if (!Number.isSafeInteger(bp)) {
thrownew RangeError(`${value} ${unit} is too large for integer basis points`);
}
return bp;
}
/** Render integer basis points in another unit. */exportfunction fromBasisPoints(basisPoints: number, unit: RateUnit): string {
if (!Number.isSafeInteger(basisPoints)) {
thrownew TypeError(`basisPoints must be a safe integer, received ${basisPoints}`);
}
return convertRate(String(basisPoints), "basis-points", unit);
}