Skip to content
markpaper

src/orders/decimal.ts

v0.3.0 · 2.2 KB

Download file
// Private exact decimal helpers for the orders module (modules keep their own copy on purpose).
// Remaining sizes after an IoC fill are computed on scaled BigInts: `0.3 - 0.1` as floats is
// `0.19999999999999998`, which would report a phantom dust remainder.

import { normalizeDecimal, type Numeric } from '../format/index.js';

/** `n / 10^s`, `s >= 0`. */
export interface Dec {
  readonly n: bigint;
  readonly s: number;
}

const PLAIN = /^(-)?(\d+)(?:\.(\d+))?$/;

/** Parses a finite number or decimal string exactly. Throws `HlFormatError` on malformed input. */
export function dec(value: Numeric): Dec {
  const text = normalizeDecimal(value);
  const m = PLAIN.exec(text);
  if (!m) throw new RangeError(`unexpected canonical decimal ${text}`);
  const frac = m[3] ?? '';
  const n = BigInt((m[2] ?? '0') + frac);
  return { n: m[1] ? -n : n, s: frac.length };
}

function align(a: Dec, b: Dec): [bigint, bigint, number] {
  if (a.s === b.s) return [a.n, b.n, a.s];
  if (a.s > b.s) return [a.n, b.n * 10n ** BigInt(a.s - b.s), a.s];
  return [a.n * 10n ** BigInt(b.s - a.s), b.n, b.s];
}

export function sub(a: Dec, b: Dec): Dec {
  const [x, y, s] = align(a, b);
  return { n: x - y, s };
}

export function mul(a: Dec, b: Dec): Dec {
  return { n: a.n * b.n, s: a.s + b.s };
}

export function cmp(a: Dec, b: Dec): -1 | 0 | 1 {
  const [x, y] = align(a, b);
  return x < y ? -1 : x > y ? 1 : 0;
}

export function abs(a: Dec): Dec {
  return a.n < 0n ? { n: -a.n, s: a.s } : a;
}

export function sign(a: Dec): -1 | 0 | 1 {
  return a.n < 0n ? -1 : a.n > 0n ? 1 : 0;
}

/** Canonical plain string (no exponent, no trailing zeros, no `-0`). */
export function plain(a: Dec): string {
  const neg = a.n < 0n;
  const digits = (neg ? -a.n : a.n).toString();
  if (a.s === 0) return normalizeDecimal(`${neg ? '-' : ''}${digits}`);
  const padded = digits.padStart(a.s + 1, '0');
  const cut = padded.length - a.s;
  return normalizeDecimal(`${neg ? '-' : ''}${padded.slice(0, cut)}.${padded.slice(cut)}`);
}

/** `max(0, a - b)` as a canonical string. */
export function nonNegativeDiff(a: Numeric, b: Numeric): string {
  const d = sub(dec(a), dec(b));
  return d.n <= 0n ? '0' : plain(d);
}
All files