257 lines
8.4 KiB
TypeScript
257 lines
8.4 KiB
TypeScript
|
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();
|