src/numbers/decimal.ts
v0.1.0 · 4.8 KB
export interface Dec {
mant: bigint;
exp: number;
}
export type SnapMode = 'floor' | 'ceil' | 'nearest';
export type SnapTolerance = 'exact' | 'float';
const QUOT_EPSILON_INV = 1000000000n;
const MAX_LITERAL_LEN = 256;
const MAX_EXP_ABS = 400;
const DEC_RE = /^(-?)(\d+)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/;
const pow10 = (n: number): bigint => 10n ** BigInt(n);
function canon(mant: bigint, exp: number): Dec {
if (mant === 0n) return { mant: 0n, exp: 0 };
let m = mant;
let e = exp;
if (e < 0) {
m *= pow10(-e);
e = 0;
}
while (e > 0 && m % 10n === 0n) {
m /= 10n;
e--;
}
return { mant: m, exp: e };
}
function checked(d: Dec, fn: string): Dec {
if (typeof d?.mant !== 'bigint' || !Number.isInteger(d.exp)) {
throw new TypeError(`qfex num.${fn}: invalid decimal (${String(d?.mant)}e-${String(d?.exp)})`);
}
return d.exp >= 0 ? d : canon(d.mant, d.exp);
}
function align(
a: Dec,
b: Dec,
): {
A: bigint;
B: bigint;
exp: number;
} {
const exp = Math.max(a.exp, b.exp);
return { A: a.mant * pow10(exp - a.exp), B: b.mant * pow10(exp - b.exp), exp };
}
function floorDiv(n: bigint, d: bigint): bigint {
const q = n / d;
return n % d !== 0n && n < 0n ? q - 1n : q;
}
function ceilDiv(n: bigint, d: bigint): bigint {
const q = n / d;
return n % d !== 0n && n > 0n ? q + 1n : q;
}
function nearestDiv(n: bigint, d: bigint): bigint {
return n >= 0n ? floorDiv(2n * n + d, 2n * d) : -floorDiv(-2n * n + d, 2n * d);
}
export function parseDec(s: unknown): Dec | null {
if (typeof s !== 'string') return null;
const t = s.trim();
if (t.length === 0 || t.length > MAX_LITERAL_LEN) return null;
const m = DEC_RE.exec(t);
if (!m) return null;
const e = m[4] === undefined ? 0 : Number(m[4]);
if (!Number.isInteger(e) || Math.abs(e) > MAX_EXP_ABS) return null;
const frac = m[3] ?? '';
const mant = BigInt(m[2] + frac);
return canon(m[1] === '-' ? -mant : mant, frac.length - e);
}
export function decFromNumber(x: number): Dec | null {
if (typeof x !== 'number' || !Number.isFinite(x)) return null;
return parseDec(String(x));
}
export function decToString(d: Dec): string {
const x = checked(d, 'decToString');
const c = canon(x.mant, x.exp);
const neg = c.mant < 0n;
const abs = neg ? -c.mant : c.mant;
if (c.exp === 0) return `${neg ? '-' : ''}${abs}`;
const digits = abs.toString().padStart(c.exp + 1, '0');
const int = digits.slice(0, -c.exp);
const frac = digits.slice(-c.exp);
return `${neg ? '-' : ''}${int}.${frac}`;
}
export function decToNumber(d: Dec): number {
return Number(decToString(d));
}
export function decCmp(a: Dec, b: Dec): -1 | 0 | 1 {
const { A, B } = align(checked(a, 'decCmp'), checked(b, 'decCmp'));
return A < B ? -1 : A > B ? 1 : 0;
}
export function decEq(a: Dec, b: Dec): boolean {
return decCmp(a, b) === 0;
}
export function decAdd(a: Dec, b: Dec): Dec {
const { A, B, exp } = align(checked(a, 'decAdd'), checked(b, 'decAdd'));
return canon(A + B, exp);
}
export function decSub(a: Dec, b: Dec): Dec {
const { A, B, exp } = align(checked(a, 'decSub'), checked(b, 'decSub'));
return canon(A - B, exp);
}
export function decMul(a: Dec, b: Dec): Dec {
const x = checked(a, 'decMul');
const y = checked(b, 'decMul');
return canon(x.mant * y.mant, x.exp + y.exp);
}
export function decNeg(d: Dec): Dec {
const x = checked(d, 'decNeg');
return canon(-x.mant, x.exp);
}
export function decAbs(d: Dec): Dec {
const x = checked(d, 'decAbs');
return canon(x.mant < 0n ? -x.mant : x.mant, x.exp);
}
export function decSign(d: Dec): -1 | 0 | 1 {
const m = checked(d, 'decSign').mant;
return m < 0n ? -1 : m > 0n ? 1 : 0;
}
export function decDecimals(d: Dec): number {
const x = checked(d, 'decDecimals');
return canon(x.mant, x.exp).exp;
}
function positiveStep(step: Dec, fn: string): Dec {
const s = checked(step, fn);
if (s.mant <= 0n) throw new RangeError(`qfex num.${fn}: step must be positive: ${decToString(s)}`);
return s;
}
export function stepsOf(x: Dec, step: Dec, mode: SnapMode, tolerance: SnapTolerance = 'exact'): bigint {
const s = positiveStep(step, 'stepsOf');
const { A, B } = align(checked(x, 'stepsOf'), s);
if (mode === 'nearest') return nearestDiv(A, B);
if (tolerance === 'float') {
const N = QUOT_EPSILON_INV;
return mode === 'floor' ? floorDiv(A * N + B, B * N) : ceilDiv(A * N - B, B * N);
}
return mode === 'floor' ? floorDiv(A, B) : ceilDiv(A, B);
}
export function snapToStep(x: Dec, step: Dec, mode: SnapMode, tolerance: SnapTolerance = 'exact'): Dec {
const s = positiveStep(step, 'snapToStep');
return canon(stepsOf(x, s, mode, tolerance) * s.mant, s.exp);
}
export function isOnStep(x: Dec, step: Dec): boolean {
const s = checked(step, 'isOnStep');
if (s.mant <= 0n) return false;
const { A, B } = align(checked(x, 'isOnStep'), s);
return A % B === 0n;
}