Skip to content

Commit

Permalink
chore(common): helpers to deal with bigint
Browse files Browse the repository at this point in the history
  • Loading branch information
stackchain committed May 4, 2024
1 parent 90dfbc7 commit d4e450d
Show file tree
Hide file tree
Showing 8 changed files with 281 additions and 20 deletions.
1 change: 1 addition & 0 deletions packages/common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export * from './utils/strings'

export * from './numbers/as-atomic-value'
export * from './numbers/split-bigint'
export * from './numbers/bigint-formatter'

export * from './observer/observer'

Expand Down
25 changes: 25 additions & 0 deletions packages/common/src/numbers/bigint-formatter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {bigintFormatter} from './bigint-formatter'

describe('bigintFormatter', () => {
// NOTE: decimal and thousands separators are locale-dependent
it('it works', () => {
let value = BigInt(1_234_567_890)
let decimalPlaces = 2
let formattedValue = bigintFormatter({value, decimalPlaces})
expect(formattedValue).toBe('12,345,678.90')

value = BigInt(189)
decimalPlaces = 6
formattedValue = bigintFormatter({value, decimalPlaces})
expect(formattedValue).toBe('0.000189')

value = BigInt(1_000_000)
formattedValue = bigintFormatter({value, decimalPlaces})
expect(formattedValue).toBe('1.000000')

value = BigInt(1_000_000)
decimalPlaces = 0
formattedValue = bigintFormatter({value, decimalPlaces})
expect(formattedValue).toBe('1,000,000')
})
})
11 changes: 11 additions & 0 deletions packages/common/src/numbers/bigint-formatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {splitBigInt} from './split-bigint'

export function bigintFormatter({
value,
decimalPlaces,
}: {
value: bigint
decimalPlaces: number
}) {
return splitBigInt(value, decimalPlaces).bn.toFormat(decimalPlaces)
}
84 changes: 78 additions & 6 deletions packages/common/src/numbers/split-bigint.test.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,104 @@
import BigNumber from 'bignumber.js'

import {splitBigInt} from './split-bigint'

describe('splitBigInt', () => {
it('should split a bigint into int dec and bn', () => {
let bigInt = BigInt(123_456_789)
let decimalPlaces = 3
let expected = {
int: BigInt(123_456),
dec: BigInt(789),
integer: '123456',
fraction: '789',
bn: new BigNumber('123456.789'),
bi: bigInt,
decimalPlaces,
str: '123456.789',
}
expect(splitBigInt(bigInt, decimalPlaces)).toEqual(expected)

bigInt = BigInt(987_654_321)
decimalPlaces = 2
expected = {
int: BigInt(9_876_543),
dec: BigInt(21),
integer: '9876543',
fraction: '21',
bn: new BigNumber('9876543.21'),
bi: bigInt,
decimalPlaces,
str: '9876543.21',
}
expect(splitBigInt(bigInt, decimalPlaces)).toEqual(expected)

bigInt = BigInt(1_432_116_543)
decimalPlaces = 5
expected = {
int: BigInt(14_321),
dec: BigInt(16_543),
integer: '14321',
fraction: '16543',
bn: new BigNumber('14321.16543'),
bi: bigInt,
decimalPlaces,
str: '14321.16543',
}
expect(splitBigInt(bigInt, decimalPlaces)).toEqual(expected)

bigInt = BigInt(123)
decimalPlaces = 6
expected = {
integer: '0',
fraction: '000123',
bn: new BigNumber('0.000123'),
bi: bigInt,
decimalPlaces,
str: '0.000123',
}
expect(splitBigInt(bigInt, decimalPlaces)).toEqual(expected)

bigInt = BigInt(123)
decimalPlaces = 3
expected = {
integer: '0',
fraction: '123',
bn: new BigNumber('0.123'),
bi: bigInt,
decimalPlaces,
str: '0.123',
}
expect(splitBigInt(bigInt, decimalPlaces)).toEqual(expected)
})

it('should handle negative bigint', () => {
let bigInt = BigInt(-123_456_789)
let decimalPlaces = 3
let expected = {
integer: '123456',
fraction: '789',
bn: new BigNumber('-123456.789'),
bi: bigInt,
decimalPlaces,
str: '-123456.789',
}
expect(splitBigInt(bigInt, decimalPlaces)).toEqual(expected)

bigInt = BigInt(-987_654_321)
decimalPlaces = 2
expected = {
integer: '9876543',
fraction: '21',
bn: new BigNumber('-9876543.21'),
bi: bigInt,
decimalPlaces,
str: '-9876543.21',
}
expect(splitBigInt(bigInt, decimalPlaces)).toEqual(expected)

bigInt = BigInt(-1_000)
decimalPlaces = 0
expected = {
integer: '1000',
fraction: '',
bn: new BigNumber('-1000'),
bi: bigInt,
decimalPlaces,
str: '-1000',
}
expect(splitBigInt(bigInt, decimalPlaces)).toEqual(expected)
})
Expand Down
34 changes: 25 additions & 9 deletions packages/common/src/numbers/split-bigint.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,33 @@
import BigNumber from 'bignumber.js'
import {freeze} from 'immer'

export function splitBigInt(
bigInt: bigint,
decimalPlaces: number,
): {int: bigint; dec: bigint} {
export function splitBigInt(bigInt: bigint, decimalPlaces: number) {
const scale: bigint = BigInt(10) ** BigInt(decimalPlaces)
const integerPart: bigint = bigInt / scale
const decimalPart: bigint = bigInt % scale

const isNegative = bigInt < 0
const sign = isNegative ? '-' : ''
const signAdjust = isNegative ? -1n : 1n

const absoluteBigInt = signAdjust * bigInt
const integer = (absoluteBigInt / scale).toString()
const fraction =
decimalPlaces > 0
? (absoluteBigInt % scale).toString().padStart(decimalPlaces, '0')
: ''
const separator = decimalPlaces > 0 ? '.' : ''

const str = `${sign}${integer}${separator}${fraction}`

const bn = new BigNumber(str)

return freeze({
int: integerPart,
dec: decimalPart,
bn: new BigNumber(`${integerPart}.${decimalPart}`),
decimalPlaces,

bi: bigInt,
integer,
fraction,

bn,
str,
})
}
9 changes: 4 additions & 5 deletions packages/portfolio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,6 @@
"<rootDir>/jest.setup.js"
]
},
"dependencies": {
"@yoroi/api": "1.5.1",
"@yoroi/common": "1.5.2",
"@yoroi/types": "1.5.4"
},
"devDependencies": {
"@commitlint/config-conventional": "^17.0.2",
"@react-native-async-storage/async-storage": "^1.19.3",
Expand All @@ -148,6 +143,8 @@
"@types/jest": "^29.5.12",
"@types/react": "^18.2.55",
"@types/react-test-renderer": "^18.0.7",
"@yoroi/types": "1.5.4",
"bignumber.js": "^9.0.1",
"commitlint": "^17.0.2",
"del-cli": "^5.0.0",
"dependency-cruiser": "^13.1.1",
Expand All @@ -171,6 +168,8 @@
},
"peerDependencies": {
"@react-native-async-storage/async-storage": "^1.19.3",
"@yoroi/api": "1.5.1",
"@yoroi/common": "1.5.2",
"bignumber.js": "^9.0.1",
"immer": "^10.0.3",
"react": ">= 16.8.0 <= 19.0.0",
Expand Down
82 changes: 82 additions & 0 deletions packages/portfolio/src/helpers/balance-formatter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {balanceFormatter} from './balance-formatter'
import {tokenBalanceMocks} from '../adapters/token-balance.mocks'

describe('balanceFormatter', () => {
it('it works', () => {
let formattedBalance = balanceFormatter({
template: '{{symbol}} {{value}} {{ticker}}',
})(tokenBalanceMocks.primaryETH)

expect(formattedBalance).toBe('Ξ 0.000000000001000000 ETH')

formattedBalance = balanceFormatter({
template: '{{ticker}} {{value}} {{symbol}}',
})(tokenBalanceMocks.primaryETH)

expect(formattedBalance).toBe('ETH 0.000000000001000000 Ξ')

formattedBalance = balanceFormatter()(tokenBalanceMocks.primaryETH)

expect(formattedBalance).toBe('0.000000000001000000')

formattedBalance = balanceFormatter({dropTraillingZeros: true})({
info: {
...tokenBalanceMocks.primaryETH.info,
decimals: 10,
},
balance: 123_456_000_000n,
})

expect(formattedBalance).toBe('12.3456')

formattedBalance = balanceFormatter({dropTraillingZeros: true})({
info: {
...tokenBalanceMocks.primaryETH.info,
decimals: 10,
},
balance: 123_456_000_100n,
})

expect(formattedBalance).toBe('12.34560001')

formattedBalance = balanceFormatter()({
info: {
...tokenBalanceMocks.primaryETH.info,
decimals: 10,
},
balance: 123_456_000_000n,
})

expect(formattedBalance).toBe('12.3456000000')

formattedBalance = balanceFormatter()({
info: {
...tokenBalanceMocks.primaryETH.info,
decimals: 10,
},
balance: 123_456_789_123_456n,
})

expect(formattedBalance).toBe('12,345.6789123456')

formattedBalance = balanceFormatter({dropTraillingZeros: true})({
info: {
...tokenBalanceMocks.primaryETH.info,
decimals: 0,
},
balance: 10n,
})

expect(formattedBalance).toBe('10')

formattedBalance = balanceFormatter({dropTraillingZeros: true})({
info: {
...tokenBalanceMocks.primaryETH.info,
decimals: 1,
},
balance: 0n,
})

expect(formattedBalance).toBe('0.0')
})
})
55 changes: 55 additions & 0 deletions packages/portfolio/src/helpers/balance-formatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {bigintFormatter} from '@yoroi/common'
import {Portfolio} from '@yoroi/types'

type BalanceFormatterConfig = Readonly<{
template?: string
dropTraillingZeros?: boolean
}>
/**
* Formats the balance of a token in a portfolio.
*
* @param config - The configuration options for the balance formatter.
* @param config.template - The template string to format the balance. Default is '{{value}}'.
* @param config.dropTraillingZeros - Whether to drop trailing zeros in the formatted balance. Default is false.
* @description The template string can contain the following placeholders: {{symbol}}, {{ticker}}, and {{value}}.
*
* @returns A function that takes a token balance and returns the formatted balance string.
*/
export function balanceFormatter({
dropTraillingZeros = false,
template = '{{value}}',
}: BalanceFormatterConfig = {}) {
return ({
balance,
info: {decimals, ticker, symbol},
}: Portfolio.Token.Balance) => {
const fmtBalance = bigintFormatter({
value: balance,
decimalPlaces: decimals,
})

let trimmedValue = fmtBalance

if (decimals > 0 && dropTraillingZeros) {
// locale dependent, so it grabs the farthest first
const decimalSeparatorIndex = Math.max(
fmtBalance.lastIndexOf(','),
fmtBalance.lastIndexOf('.'),
)
const nonZeroIndex = fmtBalance
.substring(decimalSeparatorIndex)
.search(/[1-9]0*$/)
if (nonZeroIndex !== -1) {
trimmedValue = fmtBalance.substring(
0,
decimalSeparatorIndex + nonZeroIndex + 1,
)
}
}

return template
.replace('{{symbol}}', symbol)
.replace('{{ticker}}', ticker)
.replace('{{value}}', trimmedValue)
}
}

0 comments on commit d4e450d

Please sign in to comment.