From e997ace7653d3f5fe433e19bc8d1e16526881603 Mon Sep 17 00:00:00 2001 From: Manthankumar Satani Date: Wed, 20 Mar 2024 15:10:43 +0530 Subject: [PATCH] feat: scale unit with native bigint --- packages/utils/src/index.ts | 1 + packages/utils/src/units/scale.test.ts | 88 ++++++++++++++++++++++++++ packages/utils/src/units/scale.ts | 71 +++++++++++++++++++++ packages/utils/src/web3.ts | 22 +------ packages/utils/test/web3.test.ts | 31 --------- 5 files changed, 161 insertions(+), 52 deletions(-) create mode 100644 packages/utils/src/units/scale.test.ts create mode 100644 packages/utils/src/units/scale.ts diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 37c3711..133e4d4 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,6 +1,7 @@ export * from './formatter' export * from './web3' export * from './units/unscale' +export * from './units/scale' export const simulateAsyncPause = (duration = 1000) => new Promise((resolve) => { diff --git a/packages/utils/src/units/scale.test.ts b/packages/utils/src/units/scale.test.ts new file mode 100644 index 0000000..203ebab --- /dev/null +++ b/packages/utils/src/units/scale.test.ts @@ -0,0 +1,88 @@ +import { expect, test } from 'vitest' + +import { scale, setAlwaysRoundDown } from './scale' + +test('converts number to unit of a given length', () => { + expect(scale(69, 1)).toMatchInlineSnapshot('690n') + expect(scale(13, 5)).toMatchInlineSnapshot('1300000n') + expect(scale(420, 10)).toMatchInlineSnapshot('4200000000000n') + expect(scale(20, 9)).toMatchInlineSnapshot('20000000000n') + expect(scale(40, 18)).toMatchInlineSnapshot('40000000000000000000n') + expect(scale(1.2345, 4)).toMatchInlineSnapshot('12345n') + expect(scale(1.0045, 4)).toMatchInlineSnapshot('10045n') + expect(scale(1.2345000, 4)).toMatchInlineSnapshot('12345n') + expect(scale('6942069420.12345678912345', 18)).toMatchInlineSnapshot('6942069420123456789123450000n') + expect(scale('6942069420.00045678912345', 18)).toMatchInlineSnapshot('6942069420000456789123450000n') + expect(scale('6942123123123069420.1234544444678912345', 50)) + .toMatchInlineSnapshot('694212312312306942012345444446789123450000000000000000000000000000000n') + expect(scale(-69, 1)).toMatchInlineSnapshot('-690n') + expect(scale(-1.2345, 4)).toMatchInlineSnapshot('-12345n') + expect(scale('-6942069420.12345678912345', 18)).toMatchInlineSnapshot('-6942069420123456789123450000n') + expect(scale('-6942123123123069420.1234544444678912345', 50)) + .toMatchInlineSnapshot('-694212312312306942012345444446789123450000000000000000000000000000000n') +}) + +test('decimals === 0', () => { + expect(scale('69.2352112312312451512412341231', 0)).toMatchInlineSnapshot('69n') + expect(scale('69.5952141234124125231523412312', 0)).toMatchInlineSnapshot('70n') + expect(scale('12301000000000000020000', 0)).toMatchInlineSnapshot('12301000000000000020000n') + expect(scale('12301000000000000020000.123', 0)).toMatchInlineSnapshot('12301000000000000020000n') + expect(scale('12301000000000000020000.5', 0)).toMatchInlineSnapshot('12301000000000000020001n') + expect(scale('99999999999999999999999.5', 0)).toMatchInlineSnapshot('100000000000000000000000n') +}) + +test('decimals < fraction length', () => { + expect(scale(69.23521, 0)).toMatchInlineSnapshot('69n') + expect(scale(69.56789, 0)).toMatchInlineSnapshot('70n') + expect(scale(69.23521, 1)).toMatchInlineSnapshot('692n') + expect(scale(69.23521, 2)).toMatchInlineSnapshot('6924n') + expect(scale(69.23221, 2)).toMatchInlineSnapshot('6923n') + expect(scale(69.23261, 3)).toMatchInlineSnapshot('69233n') + expect(scale(999999.99999, 3)).toMatchInlineSnapshot('1000000000n') + expect(scale(699999.99999, 3)).toMatchInlineSnapshot('700000000n') + expect(scale(699999.98999, 3)).toMatchInlineSnapshot('699999990n') + expect(scale(699959.99999, 3)).toMatchInlineSnapshot('699960000n') + expect(scale(699099.99999, 3)).toMatchInlineSnapshot('699100000n') + expect(scale(100000.000999, 3)).toMatchInlineSnapshot('100000001n') + expect(scale(100000.990999, 3)).toMatchInlineSnapshot('100000991n') + expect(scale(69.00221, 3)).toMatchInlineSnapshot('69002n') + expect(scale('1.0536059576998882', 7)).toMatchInlineSnapshot('10536060n') + expect(scale('1.0053059576998882', 7)).toMatchInlineSnapshot('10053060n') + expect(scale('1.0000000900000000', 7)).toMatchInlineSnapshot('10000001n') + expect(scale('1.0000009900000000', 7)).toMatchInlineSnapshot('10000010n') + expect(scale('1.0000099900000000', 7)).toMatchInlineSnapshot('10000100n') + expect(scale('1.0000092900000000', 7)).toMatchInlineSnapshot('10000093n') + expect(scale('1.5536059576998882', 7)).toMatchInlineSnapshot('15536060n') + expect(scale('1.0536059476998882', 7)).toMatchInlineSnapshot('10536059n') + expect(scale('1.4545454545454545', 7)).toMatchInlineSnapshot('14545455n') + expect(scale('1.1234567891234567', 7)).toMatchInlineSnapshot('11234568n') + expect(scale('1.8989898989898989', 7)).toMatchInlineSnapshot('18989899n') + expect(scale('9.9999999999999999', 7)).toMatchInlineSnapshot('100000000n') + expect(scale('0.0536059576998882', 7)).toMatchInlineSnapshot('536060n') + expect(scale('0.0053059576998882', 7)).toMatchInlineSnapshot('53060n') + expect(scale('0.0000000900000000', 7)).toMatchInlineSnapshot('1n') + expect(scale('0.0000009900000000', 7)).toMatchInlineSnapshot('10n') + expect(scale('0.0000099900000000', 7)).toMatchInlineSnapshot('100n') + expect(scale('0.0000092900000000', 7)).toMatchInlineSnapshot('93n') + expect(scale('0.0999999999999999', 7)).toMatchInlineSnapshot('1000000n') + expect(scale('0.0099999999999999', 7)).toMatchInlineSnapshot('100000n') + expect(scale('0.00000000059', 9)).toMatchInlineSnapshot('1n') + expect(scale('0.0000000003', 9)).toMatchInlineSnapshot('0n') + expect(scale('69.00000000000', 9)).toMatchInlineSnapshot('69000000000n') + expect(scale('69.00000000019', 9)).toMatchInlineSnapshot('69000000000n') + expect(scale('69.00000000059', 9)).toMatchInlineSnapshot('69000000001n') + expect(scale('69.59000000059', 9)).toMatchInlineSnapshot('69590000001n') + expect(scale('69.59000002359', 9)).toMatchInlineSnapshot('69590000024n') + expect(scale('100.00000023', 6)).toMatchInlineSnapshot('100000000n') +}) + +// round down with striping extra decimals +test('decimals > fraction length with always round down', () => { + expect(scale(69.9, 0)).toMatchInlineSnapshot('70n') + expect(scale(69.23521, 0)).toMatchInlineSnapshot('69n') + expect(scale(69.23521, 2)).toMatchInlineSnapshot('6924n') + setAlwaysRoundDown(true) + expect(scale(69.9, 0)).toMatchInlineSnapshot('69n') + expect(scale(69.23521, 2)).toMatchInlineSnapshot('6923n') + setAlwaysRoundDown(true) +}) diff --git a/packages/utils/src/units/scale.ts b/packages/utils/src/units/scale.ts new file mode 100644 index 0000000..cdc9943 --- /dev/null +++ b/packages/utils/src/units/scale.ts @@ -0,0 +1,71 @@ +let isAlwaysRoundDown = false + +/** + * Set the default rounding behavior for the scale function. If true, the scale function will always round down. + * If false, the scale function will round off. + * @notice use with caution, as this will affect all calls to the scale function. + * @warning use if you want your entire application to always round down. + */ +export function setAlwaysRoundDown(value: boolean) { + isAlwaysRoundDown = value +} + +/** + * Multiplies a string representation of a number by a given exponent of base 10 (10exponent). + * @returns A BigInt number + * @example + * scale('112', 18) // 112000000000000000000n + * scale(112, 18) // 112000000000000000000n + * scale(112.5632, 0) // 113n + * scale(112.5632, 0, true) // 112n + */ +export function scale(value: string | number, decimals: number, roundDown = isAlwaysRoundDown) { + let [integer, fraction = '0'] = `${value}`.split('.') + + const negative = integer.startsWith('-') + if (negative) + integer = integer.slice(1) + + // trim leading zeros. + fraction = fraction.replace(/(0+)$/, '') + + // round off if the fraction is larger than the number of decimals. + if (decimals === 0) { + if (Math.round(Number(`.${fraction}`)) === 1 && !roundDown) + integer = `${BigInt(integer) + BigInt(1)}` + fraction = '' + } + else if (fraction.length > decimals && !roundDown) { + const left = fraction.slice(0, decimals - 1) + const unit = fraction.slice(decimals - 1, decimals) + const right = fraction.slice(decimals) + + const rounded = Math.round(Number(`${unit}.${right}`)) + if (rounded > 9) + fraction = `${BigInt(left) + BigInt(1)}0`.padStart(left.length + 1, '0') + else fraction = `${left}${rounded}` + + if (fraction.length > decimals) { + fraction = fraction.slice(1) + integer = `${BigInt(integer) + BigInt(1)}` + } + + fraction = fraction.slice(0, decimals) + } + else if (fraction.length > decimals && roundDown) { + fraction = fraction.slice(0, decimals) + } + else { + fraction = fraction.padEnd(decimals, '0') + } + + return BigInt(`${negative ? '-' : ''}${integer}${fraction}`) +} + +/** + * wrapper for scale function that returns a string representation of the result. + * @deprecated use scale().toString() instead + */ +export function scaleToString(value: string | number, decimals: number, roundDown = isAlwaysRoundDown) { + return scale(value, decimals, roundDown).toString() +} diff --git a/packages/utils/src/web3.ts b/packages/utils/src/web3.ts index 0b68983..c09710e 100644 --- a/packages/utils/src/web3.ts +++ b/packages/utils/src/web3.ts @@ -40,30 +40,10 @@ export type BigNumberish = BN | bigint | string | number * @param decimals The number of decimals to scale to * @returns The scaled number (e.g. 1 with 6 decimal -> 1000000) */ -export const scale = (amount: string | number | bigint, decimals: BigNumberish = 6): BN => { +const scale = (amount: string | number | bigint, decimals: BigNumberish = 6): BN => { return parseUnits(amount.toString(), decimals) } -/** - * Scale the given number to the given decimals (e.g. 1 with 6 decimal -> 1000000) - * @param amount The number to scale - * @param decimals The number of decimals to scale to - * @returns The scaled number in string (e.g. 1 with 6 decimal -> "1000000") - */ -export const scaleToString = (amount: string | number | bigint, decimals: BigNumberish = 6): string => { - return parseUnits(amount.toString(), decimals).toString() -} - -/** - * sanitize and scale the given user number to the given decimals (e.g. 100.00000023 with 6 decimal -> 1000000) - * @param amount The number to scale - * @param decimals The number of decimals to sanitize with and scale to - * @returns The scaled number in string (e.g. 100.00000023 with 6 decimal -> 1000000) - */ -export const scaleUserAmount = (amount: string | number, decimals = 6) => { - return scaleToString(shortenDecimals(amount, decimals), decimals) -} - /** * UnScale the given base number to the given decimals (e.g. 1000000000000000000000012 with 18 decimal -> 1000000) * @param amount The number to un scale diff --git a/packages/utils/test/web3.test.ts b/packages/utils/test/web3.test.ts index 1d4f9ca..4c84cea 100644 --- a/packages/utils/test/web3.test.ts +++ b/packages/utils/test/web3.test.ts @@ -5,7 +5,6 @@ import { calcTotalPrice, calcUnitPrice, calcUnits, decreaseNumByPercentage, getFormattedAmount, getPercentOfAmount, getPercentageOfAmount, increaseNumByPercentage, - scale, scaleToString, scaleUserAmount, unScaleToBase, } from '../src' @@ -85,36 +84,6 @@ describe('getPercentOfAmount', () => { }) }) -describe('scale', () => { - it('Scale amount by given decimals and returns BN', () => { - const num = '12' - const decimals = 6 - const result = scale(num, decimals) - expect(result).toEqual(BN.from(12000000)) - expect(formatUnits(result, decimals)).toEqual('12.0') - }) -}) - -describe('scaleToString', () => { - it('Scale amount by given decimals and returns string', () => { - const num = '45.36624' - const decimals = 6 - const result = scaleToString(num, decimals) - expect(result).toEqual('45366240') - expect(formatUnits(result, decimals)).toEqual('45.36624') - }) -}) - -describe('scaleUserAmount', () => { - it('Scale and Sanitize amount by given decimals and returns string', () => { - const num = '45.366249072' - const decimals = 6 - const result = scaleUserAmount(num, decimals) - expect(result).toEqual('45366249') - expect(formatUnits(result, decimals)).toEqual('45.366249') - }) -}) - describe('unScaleToBase', () => { it('unScale to base amount by given decimals and returns string with no decimals', () => { const num = '45366240903847'