src/risk/decimal.test.ts
v0.3.0 · 3.2 KB
import { describe, expect, it } from 'vitest';
import {
dec,
decAbs,
decAdd,
decCmp,
decDiv,
decMul,
decNeg,
decShift,
decSign,
decSub,
decSum,
decToNumber,
decToString,
tryDec,
} from './decimal.js';
describe('decimal', () => {
it('parses plain, signed, fractional and exponent notation', () => {
expect(decToString('80')).toBe('80');
expect(decToString('-0.0016')).toBe('-0.0016');
expect(decToString('+.5')).toBe('0.5');
expect(decToString('5.')).toBe('5');
expect(decToString('1e-7')).toBe('0.0000001');
expect(decToString('2.5E3')).toBe('2500');
expect(decToString('0.000')).toBe('0');
expect(decToString(0.00015)).toBe('0.00015');
expect(decToString(1e21)).toBe('1000000000000000000000');
expect(decToString(12n)).toBe('12');
});
it('normalizes trailing zeros', () => {
expect(dec('1.2300')).toEqual({ c: 123n, e: 2 });
expect(dec('100')).toEqual({ c: 100n, e: 0 });
});
it('rejects malformed and non-finite input', () => {
expect(() => dec('')).toThrow(RangeError);
expect(() => dec('.')).toThrow(RangeError);
expect(() => dec('1.2.3')).toThrow(RangeError);
expect(() => dec('abc')).toThrow(RangeError);
expect(() => dec(Number.NaN)).toThrow(RangeError);
expect(() => dec(Number.POSITIVE_INFINITY)).toThrow(RangeError);
expect(() => dec('1e999999')).toThrow(RangeError);
expect(tryDec('x')).toBeNull();
expect(tryDec(undefined)).toBeNull();
expect(tryDec({})).toBeNull();
});
it('avoids binary float drift', () => {
expect(1.5 * 0.6).not.toBe(0.9);
expect(decToNumber(decMul(1.5, 0.6))).toBe(0.9);
expect(0.1 + 0.2).not.toBe(0.3);
expect(decToNumber(decAdd('0.1', '0.2'))).toBe(0.3);
expect(decToNumber(decSum(Array.from({ length: 1000 }, () => '0.0016')))).toBe(1.6);
});
it('adds, subtracts, negates and compares with mixed scales', () => {
expect(decToString(decSub('1', '0.4'))).toBe('0.6');
expect(decToString(decAdd('-0.5', '0.25'))).toBe('-0.25');
expect(decToString(decNeg('3.1'))).toBe('-3.1');
expect(decToString(decAbs('-3.1'))).toBe('3.1');
expect(decCmp('0.10', '0.1')).toBe(0);
expect(decCmp('-1', '0.001')).toBe(-1);
expect(decSign('-0.0')).toBe(0);
expect(decSign('-2')).toBe(-1);
});
it('divides with half-away-from-zero rounding at the scale', () => {
expect(3.3 / 3).not.toBe(1.1);
expect(decToNumber(decDiv('3.3', '3'))).toBe(1.1);
expect(decToString(decDiv('1', '3', 4))).toBe('0.3333');
expect(decToString(decDiv('2', '3', 4))).toBe('0.6667');
expect(decToString(decDiv('-2', '3', 4))).toBe('-0.6667');
expect(decToString(decDiv('1', '-8', 2))).toBe('-0.13');
expect(decToString(decDiv('0.0005', '0.25'))).toBe('0.002');
expect(decToString(decDiv('12000', '0.003', 0))).toBe('4000000');
expect(() => decDiv('1', '0')).toThrow(RangeError);
expect(() => decDiv('1', '2', -1)).toThrow(RangeError);
});
it('shifts by powers of ten in both directions', () => {
expect(decToString(decShift('1.5', -4))).toBe('0.00015');
expect(decToString(decShift('0.00045', 4))).toBe('4.5');
expect(decToString(decShift('25', 3))).toBe('25000');
expect(() => decShift('1', 0.5)).toThrow(RangeError);
});
});