SPB Git

Repository Public

decimal.test.ts 11.5 KB 235 lines typescript

1import { describe, expect, it } from 'vitest';2import { D, DEC_RE, MAX_DIGITS, type Dec } from '../src/index.ts';3import { d, prng, randDec, randDecStr, randInt, s } from './helpers.ts';45const N = 400;67describe('decimal: parsing', () => {8  it('accepts canonical decimals and normalises the output', () => {9    const cases: [string, string][] = [10      ['0', '0'], ['-0', '0'], ['-0.000', '0'], ['0.0', '0'], ['1', '1'], ['-1', '-1'], ['1.50', '1.5'], ['1.05', '1.05'], ['100', '100'],11      ['0.001', '0.001'], ['-0.001', '-0.001'], ['123456789.123456789', '123456789.123456789'], ['10.0100', '10.01'], ['1000000.000000', '1000000'],12    ];13    for (const [input, out] of cases) expect(s(D.from(input)), input).toBe(out);14  });1516  it('rejects everything that is not a canonical decimal string', () => {17    const bad = ['', ' ', '+1', '1e5', '1E5', '1e-5', 'NaN', 'Infinity', '-Infinity', '.5', '1.', '01', '00', '-01', ' 1', '1 ', '--1', '1,5', '1.2.3', '0x10', '1_000', '-', '.', '-.5', '1n', '1', '1\n'];18    for (const x of bad) expect(() => D.from(x), JSON.stringify(x)).toThrow(RangeError);19  });2021  it('enforces the digit limit', () => {22    const ok = '9'.repeat(MAX_DIGITS);23    expect(s(D.from(ok))).toBe(ok);24    expect(s(D.from(`${'9'.repeat(30)}.${'9'.repeat(MAX_DIGITS - 30)}`))).toBe(`${'9'.repeat(30)}.${'9'.repeat(MAX_DIGITS - 30)}`);25    expect(() => D.from('9'.repeat(MAX_DIGITS + 1))).toThrow(RangeError);26    expect(() => D.from(`1.${'0'.repeat(MAX_DIGITS)}`)).toThrow(RangeError);27    expect(() => D.from('1'.repeat(5000))).toThrow(RangeError);28  });2930  it('refuses numbers and other types at run time', () => {31    for (const x of [1, 1.5, NaN, null, undefined, true, {}, { m: 1, s: 0 }, { m: 1n, s: -1 }, { m: 1n, s: 1.5 }]) {32      expect(() => D.from(x as unknown as string)).toThrow(TypeError);33    }34  });3536  it('builds from bigint and from a (possibly unnormalised) Dec', () => {37    expect(s(D.from(42n))).toBe('42');38    expect(s(D.from(-7n))).toBe('-7');39    expect(s(D.from({ m: 1500n, s: 3 }))).toBe('1.5');40    expect(D.from({ m: 1500n, s: 3 })).toEqual({ m: 15n, s: 1 });41    expect(s({ m: 0n, s: 5 })).toBe('0');42    expect(s({ m: -5n, s: 3 })).toBe('-0.005');43  });4445  it('isValid mirrors from()', () => {46    expect(D.isValid('1.5')).toBe(true);47    expect(D.isValid('1e5')).toBe(false);48    expect(D.isValid(1.5)).toBe(false);49    expect(D.isValid('9'.repeat(MAX_DIGITS + 1))).toBe(false);50  });51});5253describe('decimal: arithmetic examples', () => {54  it('adds, subtracts and multiplies exactly', () => {55    expect(s(D.add(d('0.1'), d('0.2')))).toBe('0.3');56    expect(s(D.sub(d('0.3'), d('0.1')))).toBe('0.2');57    expect(s(D.mul(d('0.1'), d('0.2')))).toBe('0.02');58    expect(s(D.mul(d('-1.5'), d('4')))).toBe('-6');59    expect(s(D.sub(d('1'), d('1.000000000000000001')))).toBe('-0.000000000000000001');60    expect(s(D.mul(d('123456789012345678901234567890'), d('0.000000000000000000000000000001')))).toBe('0.12345678901234567890123456789');61  });6263  it('never prints -0', () => {64    expect(s(D.neg(d('0')))).toBe('0');65    expect(s(D.mul(d('-1'), d('0')))).toBe('0');66    expect(s(D.sub(d('-1.5'), d('-1.5')))).toBe('0');67    expect(s(D.div(d('-1'), d('3'), 0, 'down'))).toBe('0');68    expect(s(D.quantize(d('-0.4'), d('1'), 'down'))).toBe('0');69  });7071  it('divides with an explicit scale and rounding mode', () => {72    expect(s(D.div(d('1'), d('3'), 4, 'down'))).toBe('0.3333');73    expect(s(D.div(d('1'), d('3'), 4, 'up'))).toBe('0.3334');74    expect(s(D.div(d('1'), d('3'), 4, 'half_even'))).toBe('0.3333');75    expect(s(D.div(d('2'), d('3'), 4, 'half_even'))).toBe('0.6667');76    expect(s(D.div(d('-1'), d('3'), 4, 'down'))).toBe('-0.3333');      // towards zero77    expect(s(D.div(d('-1'), d('3'), 4, 'up'))).toBe('-0.3334');        // away from zero78    expect(s(D.div(d('1'), d('-3'), 4, 'up'))).toBe('-0.3334');79    expect(s(D.div(d('-1'), d('-3'), 4, 'up'))).toBe('0.3334');80    expect(s(D.div(d('10'), d('4'), 0, 'down'))).toBe('2');81    expect(s(D.div(d('10'), d('4'), 0, 'up'))).toBe('3');82    expect(s(D.div(d('6'), d('3'), 0, 'up'))).toBe('2');               // exact: no rounding in any mode83    expect(s(D.div(d('0.006'), d('0.3'), 2, 'up'))).toBe('0.02');84    expect(s(D.div(d('0'), d('7'), 5, 'up'))).toBe('0');85  });8687  it('half_even breaks ties to the even digit', () => {88    expect(s(D.div(d('0.125'), d('1'), 2, 'half_even'))).toBe('0.12');89    expect(s(D.div(d('0.135'), d('1'), 2, 'half_even'))).toBe('0.14');90    expect(s(D.div(d('0.375'), d('1'), 2, 'half_even'))).toBe('0.38');91    expect(s(D.div(d('-0.125'), d('1'), 2, 'half_even'))).toBe('-0.12');92    expect(s(D.div(d('-0.135'), d('1'), 2, 'half_even'))).toBe('-0.14');93    expect(s(D.div(d('5'), d('2'), 0, 'half_even'))).toBe('2');94    expect(s(D.div(d('7'), d('2'), 0, 'half_even'))).toBe('4');95    expect(s(D.div(d('0.1251'), d('1'), 2, 'half_even'))).toBe('0.13');96  });9798  it('refuses division by zero and bad scales or modes', () => {99    expect(() => D.div(d('1'), d('0'), 2, 'down')).toThrow(/division by zero/);100    expect(() => D.div(d('1'), d('3'), -1, 'down')).toThrow(RangeError);101    expect(() => D.div(d('1'), d('3'), 1.5, 'down')).toThrow(RangeError);102    expect(() => D.div(d('1'), d('3'), 1000, 'down')).toThrow(RangeError);103    expect(() => D.div(d('1'), d('3'), 2, 'nearest' as 'down')).toThrow(RangeError);104  });105106  it('quantizes to a tick or lot', () => {107    expect(s(D.quantize(d('1.2345'), d('0.01'), 'down'))).toBe('1.23');108    expect(s(D.quantize(d('1.2345'), d('0.01'), 'up'))).toBe('1.24');109    expect(s(D.quantize(d('-1.2345'), d('0.01'), 'down'))).toBe('-1.23');110    expect(s(D.quantize(d('-1.2345'), d('0.01'), 'up'))).toBe('-1.24');111    expect(s(D.quantize(d('1.3'), d('0.5'), 'down'))).toBe('1');112    expect(s(D.quantize(d('1.3'), d('0.5'), 'up'))).toBe('1.5');113    expect(s(D.quantize(d('60123.7'), d('5'), 'down'))).toBe('60120');114    expect(s(D.quantize(d('60123.7'), d('5'), 'up'))).toBe('60125');115    expect(s(D.quantize(d('1.5'), d('0.5'), 'up'))).toBe('1.5');116    expect(s(D.quantize(d('0.00012'), d('0.001'), 'down'))).toBe('0');117    expect(() => D.quantize(d('1'), d('0'), 'down')).toThrow(RangeError);118    expect(() => D.quantize(d('1'), d('-0.1'), 'down')).toThrow(RangeError);119    expect(() => D.quantize(d('1'), d('0.1'), 'half_even' as 'down')).toThrow(RangeError);120  });121122  it('compares, negates, takes absolute values', () => {123    expect(D.cmp(d('1.10'), d('1.1'))).toBe(0);124    expect(D.cmp(d('-2'), d('1'))).toBe(-1);125    expect(D.cmp(d('0.0000001'), d('0'))).toBe(1);126    expect(s(D.abs(d('-3.2')))).toBe('3.2');127    expect(s(D.neg(d('3.2')))).toBe('-3.2');128    expect(D.isZero(d('0.000'))).toBe(true);129    expect(D.isZero(d('0.0001'))).toBe(false);130    expect(s(D.min(d('1'), d('2')))).toBe('1');131    expect(s(D.max(d('1'), d('2')))).toBe('2');132    expect(D.isMultiple(d('0.3'), d('0.1'))).toBe(true);133    expect(D.isMultiple(d('0.35'), d('0.1'))).toBe(false);134    expect(D.toNumber(d('1.25'))).toBe(1.25);135  });136});137138describe('decimal: properties (deterministic PRNG)', () => {139  const eq = (a: Dec, b: Dec): void => { expect(s(a)).toBe(s(b)); };140141  it('add and mul are commutative and associative; mul distributes over add', () => {142    const r = prng(1);143    for (let i = 0; i < N; i++) {144      const a = randDec(r, { signed: true }), b = randDec(r, { signed: true }), c = randDec(r, { signed: true });145      eq(D.add(a, b), D.add(b, a));146      eq(D.mul(a, b), D.mul(b, a));147      eq(D.add(D.add(a, b), c), D.add(a, D.add(b, c)));148      eq(D.mul(D.mul(a, b), c), D.mul(a, D.mul(b, c)));149      eq(D.mul(a, D.add(b, c)), D.add(D.mul(a, b), D.mul(a, c)));150    }151  });152153  it('a − a = 0, a + 0 = a, a · 1 = a, a + (−a) = 0', () => {154    const r = prng(2);155    for (let i = 0; i < N; i++) {156      const a = randDec(r, { signed: true });157      expect(s(D.sub(a, a))).toBe('0');158      eq(D.add(a, D.zero), a);159      eq(D.mul(a, D.one), a);160      expect(D.isZero(D.add(a, D.neg(a)))).toBe(true);161    }162  });163164  it('(a · b) / b gives a back, and a / b · b is within one unit of the last place of a', () => {165    const r = prng(3);166    const ulp = d('0.000000000001');           // 12 decimals167    for (let i = 0; i < N; i++) {168      const a = randDec(r, { signed: true }), b = randDec(r, { signed: true, positive: true });169      // a has at most 9 decimals, so it is representable at scale 20: the division must be exact in every mode170      for (const mode of ['down', 'up', 'half_even'] as const) eq(D.div(D.mul(a, b), b, 20, mode), a);171      const down = D.div(a, b, 12, 'down'), up = D.div(a, b, 12, 'up'), he = D.div(a, b, 12, 'half_even');172      // |down| ≤ |a/b| ≤ |up|, at most one unit apart, and half_even is one of the two173      expect(D.lte(D.abs(D.mul(down, b)), D.abs(a))).toBe(true);174      expect(D.gte(D.abs(D.mul(up, b)), D.abs(a))).toBe(true);175      expect(D.lte(D.abs(D.sub(up, down)), ulp)).toBe(true);176      expect(D.eq(he, down) || D.eq(he, up)).toBe(true);177      // the error of each result, multiplied back, is below |b| · ulp178      for (const q of [down, up, he]) expect(D.lt(D.abs(D.sub(D.mul(q, b), a)), D.mul(D.abs(b), ulp))).toBe(true);179    }180  });181182  it('quantize(a, step) is a multiple of step, within one step of a, on the right side', () => {183    const r = prng(4);184    for (let i = 0; i < N; i++) {185      const a = randDec(r, { signed: true }), step = randDec(r, { intDigits: 3, fracDigits: 6, positive: true });186      const down = D.quantize(a, step, 'down'), up = D.quantize(a, step, 'up');187      for (const q of [down, up]) {188        expect(D.isMultiple(q, step)).toBe(true);189        expect(D.lt(D.abs(D.sub(q, a)), step)).toBe(true);190      }191      expect(D.lte(D.abs(down), D.abs(a))).toBe(true);192      expect(D.gte(D.abs(up), D.abs(a))).toBe(true);193      if (D.isMultiple(a, step)) { eq(down, a); eq(up, a); } else eq(D.abs(D.sub(up, down)), step);194      eq(D.quantize(down, step, 'up'), down);           // idempotent195    }196  });197198  it('comparison is consistent with subtraction and is a total order', () => {199    const r = prng(5);200    for (let i = 0; i < N; i++) {201      const a = randDec(r, { signed: true, intDigits: 3, fracDigits: 3 }), b = randDec(r, { signed: true, intDigits: 3, fracDigits: 3 }), c = randDec(r, { signed: true, intDigits: 3, fracDigits: 3 });202      expect(D.cmp(a, b)).toBe(D.sign(D.sub(a, b)));203      expect(D.cmp(a, b)).toBe(-D.cmp(b, a) || 0);204      if (D.lte(a, b) && D.lte(b, c)) expect(D.lte(a, c)).toBe(true);205      expect(D.cmp(D.add(a, c), D.add(b, c))).toBe(D.cmp(a, b));     // translation invariant206    }207  });208209  it('round-trips: str(from(s)) is the normalised s, from(str(x)) = x, and the output is canonical', () => {210    const r = prng(6);211    for (let i = 0; i < N; i++) {212      const text = randDecStr(r, { signed: true, intDigits: 20, fracDigits: 20 });213      const x = D.from(text);214      const out = D.str(x);215      expect(out).toMatch(DEC_RE);216      expect(out).not.toMatch(/^-0(\.0*)?$/);217      if (out.includes('.')) expect(out.endsWith('0')).toBe(false);218      eq(D.from(out), x);219      expect(D.str(D.from(out))).toBe(out);220      // same value as the input: compare through an independent bigint scaling221      const [i0 = '', f0 = ''] = text.replace('-', '').split('.');222      const big = BigInt(i0 + f0.padEnd(20, '0')) * (text.startsWith('-') ? -1n : 1n);223      eq(D.mul(x, D.from(10n ** 20n)), D.from(big));224    }225  });226227  it('a random unnormalised Dec prints like its normalised form', () => {228    const r = prng(7);229    for (let i = 0; i < N; i++) {230      const m = BigInt(randInt(r, -999999, 999999)), sc = randInt(r, 0, 8), pad = randInt(r, 0, 5);231      expect(D.str({ m: m * 10n ** BigInt(pad), s: sc + pad })).toBe(D.str({ m, s: sc }));232    }233  });234});235
Files main
Menu

Theme