diff --git a/src/numeric.ts b/src/numeric.ts index 08c4d95..fd9a251 100644 --- a/src/numeric.ts +++ b/src/numeric.ts @@ -112,11 +112,13 @@ export const bigNumberToString = ( decimals: number ): string => { let string = number.toString(); + const prefix = string.substring(0, 1) === '-' ? '-' : ''; + string = string.replace('-', ''); while (string.length < decimals) string = `0${string}`; const decimalLocation = string.length - decimals; const whole = string.slice(0, decimalLocation) || '0'; const fraction = string.slice(decimalLocation).replace(/0+$/, ''); - return whole + (fraction ? `.${fraction}` : ''); + return prefix + whole + (fraction ? `.${fraction}` : ''); }; export const stringToBigNumber = ( @@ -124,8 +126,9 @@ export const stringToBigNumber = ( decimals: number ): BigNumber => { if (!value || value === '.') throw new Error('errors.invalidNumber'); - if (value.substring(0, 1) === '-') throw new Error('errors.negativeNumber'); value = value.replace(/,/g, ''); + const multiplier = value.substring(0, 1) === '-' ? -1 : 1; + value = value.replace('-', ''); const [num, power] = value.split('e'); @@ -143,8 +146,8 @@ export const stringToBigNumber = ( } const base = BigNumber.from(10).pow(BigNumber.from(decimals)); - return BigNumber.from(whole).mul(base).add(fraction); + return BigNumber.from(whole).mul(base).add(fraction).mul(multiplier); } const base = BigNumber.from(10).pow(BigNumber.from(-decimals)); - return BigNumber.from(whole).div(base); + return BigNumber.from(whole).div(base).mul(multiplier); }; diff --git a/test/numeric.spec.ts b/test/numeric.spec.ts index 6dc7fc1..5589726 100644 --- a/test/numeric.spec.ts +++ b/test/numeric.spec.ts @@ -128,20 +128,6 @@ test('stringToBigNumber should accept scientific notation', () => { }); }); -test('stringToBigNumber should error for negative numbers', () => { - const testCases = [ - { value: '-100', decimals: 2 }, - { value: '-0', decimals: 2 }, - { value: '-1', decimals: 2 }, - { value: (100 - 300).toString(), decimals: 2 }, - ]; - testCases.forEach(({ value, decimals }) => { - expect(() => stringToBigNumber(value, decimals)).toThrowError( - 'errors.negativeNumber' - ); - }); -}); - test('stringToBigNumber should error for invalid numbers', () => { const testCases = [ { value: '', decimals: 2 }, diff --git a/test/scaled-number.spec.ts b/test/scaled-number.spec.ts index 8dd6cb9..d828814 100644 --- a/test/scaled-number.spec.ts +++ b/test/scaled-number.spec.ts @@ -5,25 +5,36 @@ import fc from 'fast-check'; test('should create from string', () => { const testCases = [ '1', + '-1', '12.34', + '-12.34', '0.123', + '-0.123', + '-0.0000123', '1010020102102', + '-1010020102102', '10100201202.2334234293', + '-10100201202.2334234293', '', ]; testCases.forEach((value: string) => ScaledNumber.fromUnscaled(value)); }); test('should create from number', () => { - const testCases = [1, 12.34, 0.123, 1010020102102]; + const testCases = [ + 1, -1, 12.34, -12.34, 0.123, -0.123, 1010020102102, -1010020102102, + ]; testCases.forEach((value: number) => ScaledNumber.fromUnscaled(value)); }); test('should create from big number', () => { const testCases = [ BigNumber.from(1), + BigNumber.from(-1), BigNumber.from(10000000000), + BigNumber.from(-10000000000), BigNumber.from(123102), + BigNumber.from(-123102), BigNumber.from(0), ]; testCases.forEach((value: BigNumber) => @@ -37,18 +48,34 @@ test('should create from plain token value', () => { value: '100000000', decimals: 8, }, + { + value: '-100000000', + decimals: 8, + }, { value: '1', decimals: 0, }, + { + value: '-1', + decimals: 0, + }, { value: '12910239123', decimals: 5, }, + { + value: '-12910239123', + decimals: 5, + }, { value: '21390120318230123021312031', decimals: 10, }, + { + value: '-21390120318230123021312031', + decimals: 10, + }, ]; testCases.forEach((value: PlainScaledNumber) => ScaledNumber.fromPlain(value) @@ -62,11 +89,21 @@ test('Should check if a token value is valid', () => { decimals: 8, expected: true, }, + { + value: '-100000000', + decimals: 8, + expected: true, + }, { value: '21390120318230123021312031', decimals: undefined, expected: true, }, + { + value: '-21390120318230123021312031', + decimals: undefined, + expected: true, + }, { value: '.', decimals: 3, @@ -75,7 +112,7 @@ test('Should check if a token value is valid', () => { { value: '-10000', decimals: 3, - expected: false, + expected: true, }, ]; testCases.forEach(({ value, decimals, expected }) => { @@ -86,19 +123,32 @@ test('Should check if a token value is valid', () => { test('should export as string from unscaled', () => { const testCases = [ { value: '1', expected: '1' }, + { value: '-1', expected: '-1' }, { value: 1, expected: '1' }, + { value: -1, expected: '-1' }, { value: '10', expected: '10' }, + { value: '-10', expected: '-10' }, { value: 10, expected: '10' }, + { value: -10, expected: '-10' }, { value: '0.1', expected: '0.1' }, + { value: '-0.1', expected: '-0.1' }, { value: 0.1, expected: '0.1' }, + { value: -0.1, expected: '-0.1' }, { value: '0.11923', expected: '0.11923' }, + { value: '-0.11923', expected: '-0.11923' }, { value: 0.11923, expected: '0.11923' }, + { value: -0.11923, expected: '-0.11923' }, { value: '12.34', expected: '12.34' }, + { value: '-12.34', expected: '-12.34' }, { value: 12.34, expected: '12.34' }, + { value: -12.34, expected: '-12.34' }, { value: '', expected: '0' }, { value: '12912309.341004102', expected: '12912309.341004102' }, + { value: '-12912309.341004102', expected: '-12912309.341004102' }, { value: '0000012912309.34100410200000', expected: '12912309.341004102' }, + { value: '-0000012912309.34100410200000', expected: '-12912309.341004102' }, { value: '000123.123120103000', expected: '123.123120103' }, + { value: '-000123.123120103000', expected: '-123.123120103' }, ]; testCases.forEach(({ value, expected }) => { @@ -113,19 +163,37 @@ test('should export as string from scaled', () => { decimals: 18, expected: '123', }, + { + value: BigNumber.from('-123000000000000000000'), + decimals: 18, + expected: '-123', + }, { value: BigNumber.from('123450000000000000000'), decimals: undefined, expected: '123.45', }, + { + value: BigNumber.from('-123450000000000000000'), + decimals: undefined, + expected: '-123.45', + }, { value: BigNumber.from('123450000000000000000'), decimals: 6, expected: '123450000000000', }, + { + value: BigNumber.from('-123450000000000000000'), + decimals: 6, + expected: '-123450000000000', + }, { value: BigNumber.from(1), decimals: 0, expected: '1' }, + { value: BigNumber.from(-1), decimals: 0, expected: '-1' }, { value: BigNumber.from(1), decimals: 5, expected: '0.00001' }, + { value: BigNumber.from(-1), decimals: 5, expected: '-0.00001' }, { value: BigNumber.from(134).div(10), decimals: 1, expected: '1.3' }, + { value: BigNumber.from(-134).div(10), decimals: 1, expected: '-1.3' }, ]; testCases.forEach(({ value, decimals, expected }) => { @@ -137,8 +205,11 @@ test('should export as string from scaled', () => { test('should export as string from plain', () => { const testCases = [ { value: { value: '71819', decimals: 4 }, expected: '7.1819' }, + { value: { value: '-71819', decimals: 4 }, expected: '-7.1819' }, { value: { value: '19102930', decimals: 10 }, expected: '0.001910293' }, + { value: { value: '-19102930', decimals: 10 }, expected: '-0.001910293' }, { value: { value: '000191', decimals: 5 }, expected: '0.00191' }, + { value: { value: '-000191', decimals: 5 }, expected: '-0.00191' }, ]; testCases.forEach(({ value, expected }) => { @@ -149,7 +220,7 @@ test('should export as string from plain', () => { test('toPlain/fromPlain should be symmetric for integers', () => { fc.assert( fc.property( - fc.integer({ min: 0 }), + fc.integer(), fc.integer({ min: 0, max: 27 }), (value: number, decimals: number) => { const scaledNumber = ScaledNumber.fromUnscaled(value, decimals); @@ -163,7 +234,7 @@ test('toPlain/fromPlain should be symmetric for integers', () => { test('toPlain/fromPlain should be symmetric for floats', () => { fc.assert( fc.property( - fc.float({ min: 0 }), + fc.float(), fc.integer({ min: 0, max: 27 }), (value: number, decimals: number) => { const scaledNumber = ScaledNumber.fromUnscaled(value, decimals); @@ -177,7 +248,7 @@ test('toPlain/fromPlain should be symmetric for floats', () => { test('toPlain/fromPlain should be symmetric for big numbers', () => { fc.assert( fc.property( - fc.integer({ min: 0 }), + fc.integer(), fc.integer({ min: 0, max: 27 }), (value: number, decimals: number) => { const scaledNumber = new ScaledNumber(BigNumber.from(value), decimals); @@ -191,7 +262,7 @@ test('toPlain/fromPlain should be symmetric for big numbers', () => { test('fromPlain/toPlain should be symmetric', () => { fc.assert( fc.property( - fc.integer({ min: 0 }), + fc.integer(), fc.integer({ min: 0, max: 27 }), (value: number, decimals: number) => { const plainValue: PlainScaledNumber = {