Skip to content

Commit

Permalink
Merge pull request #2 from backdfund/negative-number-support
Browse files Browse the repository at this point in the history
Add Support for Negative Numbers
  • Loading branch information
chase-manning committed May 25, 2022
2 parents c4b75c5 + 1e2e713 commit 4cd48cb
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 24 deletions.
11 changes: 7 additions & 4 deletions src/numeric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,20 +112,23 @@ 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 = (
value: string,
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');

Expand All @@ -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);
};
14 changes: 0 additions & 14 deletions test/numeric.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
83 changes: 77 additions & 6 deletions test/scaled-number.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand All @@ -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)
Expand All @@ -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,
Expand All @@ -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 }) => {
Expand All @@ -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 }) => {
Expand All @@ -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 }) => {
Expand All @@ -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 }) => {
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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 = {
Expand Down

0 comments on commit 4cd48cb

Please sign in to comment.