src/orders/decimal.ts
v0.3.0 · 2.2 KB
// 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);
}