Skip to content

Commit

Permalink
feat: improve math with formats/parsing unit related functions (#518)
Browse files Browse the repository at this point in the history
  • Loading branch information
LuizAsFight committed Sep 26, 2022
1 parent 039f930 commit 658b065
Show file tree
Hide file tree
Showing 9 changed files with 327 additions and 12 deletions.
5 changes: 5 additions & 0 deletions .changeset/stupid-pears-learn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@fuel-ts/math": patch
---

Added format/parsing helpers to `bn`. Now `bn` accepts `undefined`
187 changes: 186 additions & 1 deletion packages/math/src/bn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { BN } from './bn';
import { bn } from './bn';
import type { BigNumberish } from './types';

describe('Math - Convert', () => {
describe('Math - BN', () => {
it('can execute operations without losing our BN reference', async () => {
let test: BN;

Expand Down Expand Up @@ -280,4 +280,189 @@ describe('Math - Convert', () => {
expect(bn(maxBytes).toHex(4)).toEqual(maxHex);
expect(() => bn(over).toHex(4)).toThrow();
});

it('should create bn with number or undefined', () => {
const inputs: { numb: number; str: string; undef?: string } = {
numb: 2,
str: '5',
};

expect(bn().toNumber()).toEqual(0);
expect(bn(inputs?.undef).toNumber()).toEqual(0);
expect(bn(inputs?.numb).toNumber()).toEqual(2);
expect(bn(inputs?.str).toNumber()).toEqual(5);
});

it('should formatUnits from default unit', () => {
expect(bn('1000000000').formatUnits()).toEqual('1.000000000');
expect(bn('2').formatUnits()).toEqual('0.000000002');
expect(bn('20000').formatUnits()).toEqual('0.000020000');
expect(bn('100000020000').formatUnits()).toEqual('100.000020000');
expect(bn('100100000020000').formatUnits()).toEqual('100100.000020000');
});

it('should formatUnits from supplied unit', () => {
expect(bn('1000000000').formatUnits(7)).toEqual('100.0000000');
expect(bn('2').formatUnits(7)).toEqual('0.0000002');
expect(bn('20000').formatUnits(7)).toEqual('0.0020000');
expect(bn('100000020000').formatUnits(7)).toEqual('10000.0020000');
expect(bn('100100000020000').formatUnits(7)).toEqual('10010000.0020000');
});

it('should format with default configs', () => {
expect(bn('1000000000').format()).toEqual('1.0');
expect(bn('2').format()).toEqual('0.000000002');
expect(bn('22000').format()).toEqual('0.00002');
expect(bn('100000020000').format()).toEqual('100.0');
expect(bn('100100000020000').format()).toEqual('100,100.0');
});

it('should format with NOT default configs', () => {
expect(
bn('1000000000').format({
minPrecision: 2,
})
).toEqual('1.00');
expect(
bn('1000000000').format({
minPrecision: 2,
units: 8,
})
).toEqual('10.00');
expect(
bn('1000000000').format({
minPrecision: 2,
units: 10,
})
).toEqual('0.10');
expect(
bn('1000000000').format({
minPrecision: 4,
precision: 3,
})
).toEqual('1.000');

expect(
bn('1123000000').format({
minPrecision: 3,
precision: 4,
})
).toEqual('1.123');
expect(
bn('1123000000').format({
minPrecision: 4,
precision: 4,
})
).toEqual('1.1230');

expect(
bn('2').format({
minPrecision: 2,
})
).toEqual('0.000000002');
expect(
bn('2').format({
minPrecision: 2,
units: 10,
})
).toEqual('0.0000000002');
expect(
bn('2').format({
minPrecision: 2,
units: 8,
})
).toEqual('0.00000002');

expect(
bn('22000').format({
minPrecision: 2,
})
).toEqual('0.00002');

expect(
bn('100000020000').format({
minPrecision: 8,
precision: 8,
})
).toEqual('100.00002000');
expect(
bn('100000020000').format({
minPrecision: 4,
precision: 8,
})
).toEqual('100.00002');
expect(
bn('100000020000').format({
minPrecision: 4,
precision: 4,
})
).toEqual('100.0000');

expect(
bn('100100000020000').format({
minPrecision: 1,
})
).toEqual('100,100.0');
expect(
bn('100100000020000').format({
minPrecision: 2,
units: 8,
})
).toEqual('1,001,000.00');

expect(
bn('100100000020000').format({
minPrecision: 3,
units: 10,
})
).toEqual('10,010.000');
expect(
bn('100100000020000').format({
units: 10,
})
).toEqual('10,010.0');

expect(
bn('1001000000200000000').format({
minPrecision: 2,
units: 8,
})
).toEqual('10,010,000,002.00');
expect(
bn('1001000000200000000').format({
units: 8,
})
).toEqual('10,010,000,002.0');
expect(
bn('1001000000200000000').format({
minPrecision: 2,
})
).toEqual('1,001,000,000.20');
expect(
bn('1001000000200000000').format({
minPrecision: 5,
})
).toEqual('1,001,000,000.200');
expect(
bn('1001000000200000000').format({
minPrecision: 5,
precision: 8,
})
).toEqual('1,001,000,000.20000');
});

it('should parse to bn unit from decimal/inputs/string values', () => {
expect(bn.parseUnits('1').toHex()).toEqual(bn('1000000000').toHex());
expect(bn.parseUnits('0.000000002').toHex()).toEqual(bn(2).toHex());
expect(bn.parseUnits('0.00002').toHex()).toEqual(bn('20000').toHex());
expect(bn.parseUnits('100.00002').toHex()).toEqual(bn('100000020000').toHex());
expect(bn.parseUnits('100,100.00002').toHex()).toEqual(bn('100100000020000').toHex());
expect(bn.parseUnits('100,100.00002', 5).toHex()).toEqual(bn('10010000002').toHex());
expect(bn.parseUnits('.').toHex()).toEqual(bn('0').toHex());
expect(bn.parseUnits('.', 5).toHex()).toEqual(bn('0').toHex());

expect(() => {
bn.parseUnits('100,100.000002', 5);
}).toThrow("Decimal can't be bigger than the units");
});
});
63 changes: 59 additions & 4 deletions packages/math/src/bn.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import BnJs from 'bn.js';

import { DECIMAL_UNITS, DEFAULT_MIN_PRECISION, DEFAULT_PRECISION } from './constants';
import { toFixed } from './decimal';
import type { FormatConfig } from './types';

type CompareResult = -1 | 0 | 1;
export type BNInput = number | string | number[] | Uint8Array | Buffer | BnJs;
interface BNHelper {
Expand Down Expand Up @@ -37,7 +41,7 @@ interface BNHiddenTypes {
type BNInputOverridesKeys = keyof BNInputOverrides;

export class BN extends BnJs implements BNInputOverrides, BNHiddenTypes, BNHelper, BNOverrides {
constructor(value: BNInput, base?: number | 'hex', endian?: BnJs.Endianness) {
constructor(value?: BNInput, base?: number | 'hex', endian?: BnJs.Endianness) {
if (BN.isBN(value)) {
super(value.toArray(), base, endian);
return;
Expand All @@ -47,8 +51,8 @@ export class BN extends BnJs implements BNInputOverrides, BNHiddenTypes, BNHelpe
super(value.substring(2), base || 'hex', endian);
return;
}

super(value, base, endian);
const defaultValue = value == null ? 0 : value;
super(defaultValue, base, endian);
}

// ANCHOR: HELPERS
Expand Down Expand Up @@ -86,6 +90,42 @@ export class BN extends BnJs implements BNInputOverrides, BNHiddenTypes, BNHelpe
toJSON(): string {
return this.toString(16);
}

format(options?: FormatConfig): string {
const {
units = DECIMAL_UNITS,
precision = DEFAULT_PRECISION,
minPrecision = DEFAULT_MIN_PRECISION,
} = options || {};

const formattedUnits = this.formatUnits(units);
const formattedFixed = toFixed(formattedUnits, { precision, minPrecision });

// increase precision if formatted is zero, but has more numbers out of precision
if (!parseFloat(formattedFixed)) {
const [, originalDecimals = '0'] = formattedUnits.split('.');
const firstNonZero = originalDecimals.match(/[1-9]/);

if (firstNonZero && firstNonZero.index && firstNonZero.index + 1 > precision) {
const [valueUnits = '0'] = formattedFixed.split('.');
return `${valueUnits}.${originalDecimals.slice(0, firstNonZero.index + 1)}`;
}
}

return formattedFixed;
}

formatUnits(units: number = DECIMAL_UNITS): string {
const valueUnits = this.toString().slice(0, units * -1);
const valueDecimals = this.toString().slice(units * -1);
const length = valueDecimals.length;
const defaultDecimals = Array.from({ length: units - length })
.fill('0')
.join('');
const integerPortion = valueUnits ? `${valueUnits}.` : '0.';

return `${integerPortion}${defaultDecimals}${valueDecimals}`;
}
// END ANCHOR: HELPERS

// ANCHOR: OVERRIDES to accept better inputs
Expand Down Expand Up @@ -211,5 +251,20 @@ export class BN extends BnJs implements BNInputOverrides, BNHiddenTypes, BNHelpe
}

// functional shortcut to create BN
export const bn = (value: BNInput, base?: number | 'hex', endian?: BnJs.Endianness) =>
export const bn = (value?: BNInput, base?: number | 'hex', endian?: BnJs.Endianness) =>
new BN(value, base, endian);

bn.parseUnits = (value: string, units: number = DECIMAL_UNITS): BN => {
const valueToParse = value === '.' ? '0.' : value;
const [valueUnits = '0', valueDecimals = '0'] = valueToParse.split('.');
const length = valueDecimals.length;

if (length > units) {
throw new Error("Decimal can't be bigger than the units");
}

const decimals = Array.from({ length: units }).fill('0');
decimals.splice(0, length, valueDecimals);
const amount = `${valueUnits.replace(',', '')}${decimals.join('')}`;
return bn(amount);
};
3 changes: 3 additions & 0 deletions packages/math/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const DEFAULT_PRECISION = 3;
export const DEFAULT_MIN_PRECISION = 1;
export const DECIMAL_UNITS = 9;
22 changes: 22 additions & 0 deletions packages/math/src/decimal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { DEFAULT_MIN_PRECISION, DEFAULT_PRECISION } from './constants';
import type { ToFixedConfig } from './types';

export function toFixed(value?: string | number, options?: ToFixedConfig) {
const { precision = DEFAULT_PRECISION, minPrecision = DEFAULT_MIN_PRECISION } = options || {};

const [valueUnits = '0', valueDecimals = '0'] = String(value || '0.0').split('.');
const groupRegex = /(\d)(?=(\d{3})+\b)/g;
const units = valueUnits.replace(groupRegex, '$1,');
let decimals = valueDecimals.slice(0, precision);

// strip traling zeros limited by minPrecision
if (minPrecision < precision) {
const firstNonZero = decimals.match(/[1-9]/);
const firstNonZeroIndex = firstNonZero?.index == null ? -1 : firstNonZero.index;
const keepChars = Math.max(minPrecision, firstNonZeroIndex + 1);
decimals = decimals.slice(0, keepChars);
}

const decimalPortion = decimals ? `.${decimals}` : '';
return `${units}${decimalPortion}`;
}
25 changes: 25 additions & 0 deletions packages/math/src/functional.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Simple tests to make sure functional shortcuts are working.
*
* Deeper tests are in ./bn.test.ts
*/

import { format, formatUnits, toBytes, toHex, toNumber } from './functional';

describe('Math - Functional shortcuts', () => {
it('should toNumber return a number', () => {
expect(toNumber('50000')).toEqual(50000);
});
it('should toHex return a Hex string', () => {
expect(toHex('50000')).toEqual('0xc350');
});
it('should toBytes return a bytes array (Uint8Array)', () => {
expect(JSON.stringify(toBytes('50000'))).toEqual(JSON.stringify({ 0: 195, 1: 80 }));
});
it('should formatUnits return a formatted string', () => {
expect(formatUnits('1000000000')).toEqual('1.000000000');
});
it('should format return a formatted string', () => {
expect(format('1000000000')).toEqual('1.0');
});
});
19 changes: 15 additions & 4 deletions packages/math/src/convert.ts → packages/math/src/functional.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
/**
* From: https://github.dev/ethers-io/ethers.js/blob/9ca3dc557de8d1556096ea4140316e7f7711a0f3/packages/math/src.ts/convert.ts
*/

import type { BNInput } from './bn';
import { bn } from './bn';
import type { FormatConfig } from './types';

/**
* Functional shortcuts
*/

// Shortcut to bn(value).toNumber
export function toNumber(value: BNInput): number {
Expand All @@ -19,3 +20,13 @@ export function toHex(value: BNInput, bytesPadding?: number): string {
export function toBytes(value: BNInput, bytesPadding?: number): Uint8Array {
return bn(value).toBytes(bytesPadding);
}

// Shortcut to bn.(value).formatUnits
export function formatUnits(value: BNInput, units?: number): string {
return bn(value).formatUnits(units);
}

// Shortcut to bn.(value).format
export function format(value: BNInput, options?: FormatConfig): string {
return bn(value).format(options);
}
8 changes: 5 additions & 3 deletions packages/math/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export * from './convert';
export * from './types';
export * from './math';
export * from './bn';
export * from './constants';
export * from './decimal';
export * from './functional';
export * from './math';
export * from './types';

1 comment on commit 658b065

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage report

St.
Category Percentage Covered / Total
🟢 Statements 89.66% 3382/3772
🟡 Branches 70.01% 649/927
🟢 Functions 86.43% 675/781
🟢 Lines 89.62% 3238/3613

Test suite run success

515 tests passing in 46 suites.

Report generated by 🧪jest coverage report action from 658b065

Please sign in to comment.