Skip to content
markpaper

src/risk/decimal.test.ts

v0.3.0 · 3.2 KB

Download file
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);
  });
});
All files