Functional Weave
Code in TypeScript

math.round-float@1.0.0

impl/typescript.ts

2,249 bytes · the TypeScript implementation · view raw

const POW10 = [1, 10, 100, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12];

// 2^52: at or above this every double is a whole number, so a scaled value
// this large has no digits left to round.
const INTEGRAL = 4503599627370496;
// Veltkamp's splitting constant, 2^27 + 1.
const SPLIT = 134217729;

/**
 * The rounding error of a * b: a * b - fl(a * b), exactly (Dekker's
 * TwoProduct). Only * and - are used, so it needs no fused multiply-add,
 * which JavaScript does not have.
 */
function productError(a: number, b: number, p: number): number {
  const ca = SPLIT * a;
  const ah = ca - (ca - a);
  const al = a - ah;
  const cb = SPLIT * b;
  const bh = cb - (cb - b);
  const bl = b - bh;
  return ((ah * bh - p) + ah * bl + al * bh) + al * bl;
}

/**
 * Round half away from zero to `decimals` places, deciding on the exact value
 * of the double.
 *
 * 2.675 is stored as 2.67499999999999982236431605997495353221893310546875,
 * so it rounds to 2.67, as JavaScript's toFixed(2) says. The shortcut
 * `Math.round(x * 100) / 100` says 2.68, because 2.675 * 100 rounds to exactly
 * 267.5 before Math.round sees it; it also rounds -2.5 to -2. Here the
 * product's rounding error is recovered exactly (TwoProduct), so the tie test
 * is on the true value, using only +, -, * and floor, which IEEE 754 defines
 * exactly in every language.
 */
export function roundFloat(value: number, decimals: number): number {
  if (typeof value !== "number" || !Number.isFinite(value)) {
    throw new TypeError(`value must be a finite number, received ${value}`);
  }
  if (!Number.isInteger(decimals) || decimals < 0 || decimals > 12) {
    throw new RangeError(`decimals must be a whole number from 0 to 12, received ${decimals}`);
  }
  const scale = POW10[decimals];
  const a = Math.abs(value);
  const y = a * scale;
  if (y >= INTEGRAL) return value + 0;
  let r = Math.floor(y);
  // y - r is exact, and so is subtracting a half from it; adding the product's
  // error cannot change the sign of the answer, only settle a tie.
  const above = (y - r - 0.5) + productError(a, scale, y);
  if (above >= 0) r += 1;
  const out = r / scale;
  // + 0 turns -0 into 0, so -0.001 rounded to 2 places is 0.
  return (value < 0 ? -out : out) + 0;
}