import { expect, tap } from '@git.zone/tstest/tapbundle'; import { Decimal, decimal, RoundingMode } from '../ts/formats/utils/decimal.js'; tap.test('Decimal - basic construction', async () => { // From string const d1 = new Decimal('123.456'); expect(d1.toString()).toEqual('123.456'); // From number const d2 = new Decimal(123.456); expect(d2.toString()).toEqual('123.456'); // From bigint const d3 = new Decimal(123n); expect(d3.toString()).toEqual('123'); // From another Decimal const d4 = new Decimal(d1); expect(d4.toString()).toEqual('123.456'); // Negative values const d5 = new Decimal('-123.456'); expect(d5.toString()).toEqual('-123.456'); }); tap.test('Decimal - arithmetic operations', async () => { const a = new Decimal('10.50'); const b = new Decimal('3.25'); // Addition expect(a.add(b).toString()).toEqual('13.75'); // Subtraction expect(a.subtract(b).toString()).toEqual('7.25'); // Multiplication expect(a.multiply(b).toString()).toEqual('34.125'); // Division expect(a.divide(b).toString()).toEqual('3.2307692307'); // Percentage const amount = new Decimal('100'); const rate = new Decimal('19'); expect(amount.percentage(rate).toString()).toEqual('19'); }); tap.test('Decimal - rounding modes', async () => { // HALF_UP (default) expect(new Decimal('2.5').round(0, 'HALF_UP').toString()).toEqual('3'); expect(new Decimal('2.4').round(0, 'HALF_UP').toString()).toEqual('2'); expect(new Decimal('-2.5').round(0, 'HALF_UP').toString()).toEqual('-3'); // HALF_DOWN expect(new Decimal('2.5').round(0, 'HALF_DOWN').toString()).toEqual('2'); expect(new Decimal('2.6').round(0, 'HALF_DOWN').toString()).toEqual('3'); expect(new Decimal('-2.5').round(0, 'HALF_DOWN').toString()).toEqual('-2'); // HALF_EVEN (Banker's rounding) expect(new Decimal('2.5').round(0, 'HALF_EVEN').toString()).toEqual('2'); expect(new Decimal('3.5').round(0, 'HALF_EVEN').toString()).toEqual('4'); expect(new Decimal('2.4').round(0, 'HALF_EVEN').toString()).toEqual('2'); expect(new Decimal('2.6').round(0, 'HALF_EVEN').toString()).toEqual('3'); // UP (away from zero) expect(new Decimal('2.1').round(0, 'UP').toString()).toEqual('3'); expect(new Decimal('-2.1').round(0, 'UP').toString()).toEqual('-3'); // DOWN (toward zero) expect(new Decimal('2.9').round(0, 'DOWN').toString()).toEqual('2'); expect(new Decimal('-2.9').round(0, 'DOWN').toString()).toEqual('-2'); // CEILING (toward positive infinity) expect(new Decimal('2.1').round(0, 'CEILING').toString()).toEqual('3'); expect(new Decimal('-2.9').round(0, 'CEILING').toString()).toEqual('-2'); // FLOOR (toward negative infinity) expect(new Decimal('2.9').round(0, 'FLOOR').toString()).toEqual('2'); expect(new Decimal('-2.1').round(0, 'FLOOR').toString()).toEqual('-3'); }); tap.test('Decimal - EN16931 calculation scenarios', async () => { // Line item calculation const quantity = new Decimal('3'); const unitPrice = new Decimal('33.333333'); const lineTotal = quantity.multiply(unitPrice); expect(lineTotal.round(2).toString()).toEqual('100'); // VAT calculation const netAmount = new Decimal('100'); const vatRate = new Decimal('19'); const vatAmount = netAmount.percentage(vatRate); expect(vatAmount.toString()).toEqual('19'); // Total with VAT const grossAmount = netAmount.add(vatAmount); expect(grossAmount.toString()).toEqual('119'); // Complex calculation with allowances const lineExtension = new Decimal('150.00'); const allowance = new Decimal('10.00'); const charge = new Decimal('5.00'); const taxExclusive = lineExtension.subtract(allowance).add(charge); expect(taxExclusive.toString()).toEqual('145'); const vat = taxExclusive.percentage(new Decimal('19')); expect(vat.round(2).toString()).toEqual('27.55'); const total = taxExclusive.add(vat); expect(total.round(2).toString()).toEqual('172.55'); }); tap.test('Decimal - comparisons', async () => { const a = new Decimal('10.50'); const b = new Decimal('10.50'); const c = new Decimal('10.51'); // Equality expect(a.equals(b)).toBeTrue(); expect(a.equals(c)).toBeFalse(); // With tolerance expect(a.equals(c, '0.01')).toBeTrue(); expect(a.equals(c, '0.005')).toBeFalse(); // Comparisons expect(a.lessThan(c)).toBeTrue(); expect(c.greaterThan(a)).toBeTrue(); expect(a.lessThanOrEqual(b)).toBeTrue(); expect(a.greaterThanOrEqual(b)).toBeTrue(); }); tap.test('Decimal - edge cases', async () => { // Very small numbers const tiny = new Decimal('0.0000000001'); expect(tiny.multiply(new Decimal('1000000000')).toString()).toEqual('0.1'); // Very large numbers const huge = new Decimal('999999999999999999'); expect(huge.add(new Decimal('1')).toString()).toEqual('1000000000000000000'); // Division by zero const zero = new Decimal('0'); const one = new Decimal('1'); let errorThrown = false; try { one.divide(zero); } catch (e) { errorThrown = true; expect(e.message).toEqual('Division by zero'); } expect(errorThrown).toBeTrue(); // Zero operations expect(zero.add(one).toString()).toEqual('1'); expect(zero.multiply(one).toString()).toEqual('0'); expect(zero.isZero()).toBeTrue(); expect(one.isZero()).toBeFalse(); }); tap.test('Decimal - currency calculations with different minor units', async () => { // EUR (2 decimal places) const eurAmount = new Decimal('100.00'); const eurVat = eurAmount.percentage(new Decimal('19')); expect(eurVat.round(2).toString()).toEqual('19'); // JPY (0 decimal places) const jpyAmount = new Decimal('1000'); const jpyTax = jpyAmount.percentage(new Decimal('10')); expect(jpyTax.round(0).toString()).toEqual('100'); // KWD (3 decimal places) const kwdAmount = new Decimal('100.000'); const kwdTax = kwdAmount.percentage(new Decimal('5')); expect(kwdTax.round(3).toString()).toEqual('5'); // BTC (8 decimal places for satoshis) const btcAmount = new Decimal('0.00100000'); const btcFee = btcAmount.percentage(new Decimal('0.1')); expect(btcFee.round(8).toString()).toEqual('0.000001'); }); tap.test('Decimal - static methods', async () => { // Sum const values = ['10.50', '20.25', '30.75']; const sum = Decimal.sum(values); expect(sum.toString()).toEqual('61.5'); // Min const min = Decimal.min('10.50', '20.25', '5.75'); expect(min.toString()).toEqual('5.75'); // Max const max = Decimal.max('10.50', '20.25', '5.75'); expect(max.toString()).toEqual('20.25'); // From percentage const rate = Decimal.fromPercentage('19%'); expect(rate.toString()).toEqual('0.19'); }); tap.test('Decimal - formatting', async () => { const value = new Decimal('1234.567890'); // Fixed decimal places expect(value.toFixed(2)).toEqual('1234.57'); expect(value.toFixed(0)).toEqual('1235'); expect(value.toFixed(4)).toEqual('1234.5679'); // toString with decimal places expect(value.toString(2)).toEqual('1234.56'); expect(value.toString(6)).toEqual('1234.567890'); // Automatic trailing zero removal const rounded = new Decimal('100.00'); expect(rounded.toString()).toEqual('100'); expect(rounded.toFixed(2)).toEqual('100.00'); }); tap.test('Decimal - real-world invoice calculation', async () => { // Invoice with multiple lines and VAT rates const lines = [ { quantity: '2', unitPrice: '50.00', vatRate: '19' }, { quantity: '3', unitPrice: '33.33', vatRate: '19' }, { quantity: '1', unitPrice: '100.00', vatRate: '7' } ]; let totalNet = Decimal.ZERO; let totalVat19 = Decimal.ZERO; let totalVat7 = Decimal.ZERO; for (const line of lines) { const quantity = new Decimal(line.quantity); const unitPrice = new Decimal(line.unitPrice); const lineNet = quantity.multiply(unitPrice); totalNet = totalNet.add(lineNet); const vatAmount = lineNet.percentage(new Decimal(line.vatRate)); if (line.vatRate === '19') { totalVat19 = totalVat19.add(vatAmount); } else { totalVat7 = totalVat7.add(vatAmount); } } expect(totalNet.round(2).toString()).toEqual('299.99'); expect(totalVat19.round(2).toString()).toEqual('38'); expect(totalVat7.round(2).toString()).toEqual('7'); const totalVat = totalVat19.add(totalVat7); const totalGross = totalNet.add(totalVat); expect(totalVat.round(2).toString()).toEqual('45'); expect(totalGross.round(2).toString()).toEqual('344.99'); }); export default tap.start();