Skip to content
This repository has been archived by the owner on Apr 4, 2022. It is now read-only.

Commit

Permalink
Merge a4a5196 into 86ea823
Browse files Browse the repository at this point in the history
  • Loading branch information
W3stside committed May 20, 2020
2 parents 86ea823 + a4a5196 commit 40ecea3
Show file tree
Hide file tree
Showing 4 changed files with 1,295 additions and 1,395 deletions.
17 changes: 17 additions & 0 deletions src/const.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import BN from 'bn.js'
import BigNumber from 'bignumber.js'
import { toWei } from 'utils/ethereum'

// Some convenient numeric constant
export const ZERO = new BN(0)
export const ONE = new BN(1)
export const TWO = new BN(2)
export const TEN = new BN(10)
export const BN_100K = new BN('100000')
export const BN_1M = new BN('1000000')
export const BN_10M = new BN('10000000')
export const BN_1B = new BN('1000000000')
export const BN_1T = new BN('1000000000000')
// BigNumber
export const ONE_BIG_NUMBER = new BigNumber(1)
export const TEN_BIG_NUMBER = new BigNumber(10)

Expand All @@ -15,8 +22,18 @@ export const ALLOWANCE_MAX_VALUE = TWO.pow(new BN(256)).sub(ONE) // 115792089237
export const ALLOWANCE_FOR_ENABLED_TOKEN = TWO.pow(new BN(128)) // 340282366920938463463374607431768211456

// Default formatting constants
export const ELLIPSIS = '...'
export const DEFAULT_DECIMALS = 4
export const DEFAULT_PRECISION = 18
// numbers over 1B / 1T
// e.g 234.543B
export const DEFAULT_LARGE_NUMBER_PRECISION = 3
// Represented as WEI to use BN comparison
// Why? Would not be possible otherwise, as
// BN does not accept fractional values
export const DEFAULT_SMALL_LIMIT_AS_WEI = new BN(toWei('0.001'))
export const DEFAULT_THOUSANDS_SYMBOL = ','
export const DEFAULT_DECIMALS_SYMBOL = '.'

// Model constants
export const FEE_DENOMINATOR = 1000 // Fee is 1/fee_denominator i.e. 1/1000 = 0.1%
Expand Down
184 changes: 170 additions & 14 deletions src/utils/format.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
import BN from 'bn.js'
import { TEN, DEFAULT_PRECISION } from 'const'
import { TokenDetails } from 'types'
import BigNumber from 'bignumber.js'
const DEFAULT_DECIMALS = 4
const ELLIPSIS = '...'
import {
TEN,
DEFAULT_PRECISION,
DEFAULT_DECIMALS,
DEFAULT_SMALL_LIMIT_AS_WEI,
DEFAULT_THOUSANDS_SYMBOL,
DEFAULT_DECIMALS_SYMBOL,
ELLIPSIS,
DEFAULT_LARGE_NUMBER_PRECISION,
BN_1T,
BN_1B,
BN_10M,
BN_1M,
BN_100K,
} from 'const'
import { toWei, fromWei } from './ethereum'
import { TokenDetails } from 'types'

function _getLocaleSymbols(): { thousands: string; decimals: string } {
// Check number representation in default locale
Expand All @@ -15,27 +28,170 @@ function _getLocaleSymbols(): { thousands: string; decimals: string } {
}

const { thousands: THOUSANDS_SYMBOL, decimals: DECIMALS_SYMBOL } = _getLocaleSymbols()
const DEFAULT_THOUSANDS_SYMBOL = ','
const DEFAULT_DECIMALS_SYMBOL = '.'

const _formatDecimalsFromWeiForDisplay = (decimalsToConvert: BN, decimalSymbol: string = DEFAULT_DECIMALS_SYMBOL) => '0' + decimalSymbol + fromWei(decimalsToConvert).slice(2)

interface CompactionOptions {
precision: number,
thousandsSymbol: string,
decimalSymbol: string
}

function _compactLargeBNNumberToString(
baseBN: BN,
representationBN: BN,
{
precision,
thousandsSymbol,
decimalSymbol,
}: CompactionOptions): string {
// e.g TRILLION_123_123.div(ONE_TRILLION) = 123123.123123123
const numAsFraction = baseBN.mul(TEN.pow(new BN(precision))).div(representationBN)
const { integerPart, decimalPart } = _decomposeBn(numAsFraction, DEFAULT_LARGE_NUMBER_PRECISION, DEFAULT_LARGE_NUMBER_PRECISION)
// 123123.123123123 = 123,123.123123123
const formattedInteger = _formatNumber(integerPart.toString(), thousandsSymbol)
// no relevant decimal section
if (decimalPart.isZero()) return formattedInteger

const formattedDecimal = decimalPart
.toString()
.padStart(precision, '0') // Pad the decimal part with leading zeros
.replace(/0+$/, '') // Remove the right zeros
return formattedInteger + decimalSymbol + formattedDecimal
}

function _formatNumber(num: string, thousandsSymbol: string = THOUSANDS_SYMBOL): string {
return num.replace(/(\d)(?=(?:\d{3})+(?!\d))/g, '$1' + thousandsSymbol)
}

function _decomposeBn(amount: BN, amountPrecision: number, decimals: number): { integerPart: BN; decimalPart: BN } {
// Discard the decimals we don't need
// i.e. for WETH (precision=18, decimals=4) --> amount / 1e14
// 16.5*1e18 ---> 165000
interface DecomposedNumberParts {
integerPart: BN
decimalPart: BN
decimalsPadded: string
}

function _formatSmart(
{ integerPart, decimalPart, decimalsPadded }: DecomposedNumberParts,
thousandsSymbol: string = THOUSANDS_SYMBOL,
decimalSymbol: string = DECIMALS_SYMBOL,
smallLimitAsWei: BN,
precision: number = DEFAULT_LARGE_NUMBER_PRECISION,
): string {
// Is fraction
if (integerPart.isZero()) {
// if amount < 1 and fraction < smallLimit (both compared as Wei)
// return `< ${smallLimit}`
// else return decimals as is
// first we need to convert decimals to WEI in order to compare small values
const ourDecimalsAsBNWei = new BN(toWei('0.' + decimalsPadded))
return ourDecimalsAsBNWei.lt(smallLimitAsWei) ? `< ${_formatDecimalsFromWeiForDisplay(smallLimitAsWei, decimalSymbol)}` : _formatDecimalsFromWeiForDisplay(ourDecimalsAsBNWei)
}

const compactionOptions = { precision, thousandsSymbol, decimalSymbol }

// Number compacting logic
// Anything > 1,000,000,000,000 denoted as <XXX,XXX.xxx>T
if (integerPart.gte(BN_1T)) {
return _compactLargeBNNumberToString(integerPart, BN_1T, compactionOptions) + 'T'
}
// Anything not above 1T but > 1,000,000,000 denoted as <XXX.xxx>B
if (integerPart.gte(BN_1B)) {
return _compactLargeBNNumberToString(integerPart, BN_1B, compactionOptions) + 'B'
}

// At this point can just return thousands separated formatted integer
// if decimals are zero
if (decimalPart.isZero()) return _formatNumber(integerPart.toString())

let finalPrecision: number

// normal format + separator
if (integerPart.lt(BN_100K)) {
// 99,999.3424
finalPrecision = 4
} else if (integerPart.lt(BN_1M)) {
// 999,999.342
finalPrecision = 3
} else if (integerPart.lt(BN_10M)) {
// 9,999,999.34
finalPrecision = 2
} else {
// 99,999,999.3
finalPrecision = 1
}

const amountBeforePrecisionCheck = _formatNumber(integerPart.toString()) + decimalSymbol + decimalsPadded
return adjustPrecision(amountBeforePrecisionCheck, finalPrecision).replace(/0+$/, '')
}

function _decomposeBn(amount: BN, amountPrecision: number, decimals: number): { integerPart: BN; decimalPart: BN, decimalsPadded: string } {
if (decimals > amountPrecision) {
throw new Error('The decimals cannot be bigger than the precision')
}
const amountRaw = amount.divRound(TEN.pow(new BN(amountPrecision - decimals)))
const integerPart = amountRaw.div(TEN.pow(new BN(decimals))) // 165000 / 10000 = 16
const decimalPart = amountRaw.mod(TEN.pow(new BN(decimals))) // 165000 % 10000 = 5000
// Discard the decimals we don't need
// i.e. for WETH (precision=18, decimals=4) --> amount / 1e14
// 1, 18: 16.5*1e18 ---> 165000
return { integerPart, decimalPart }
const amountRaw = amount.divRound(TEN.pow(new BN(amountPrecision - decimals)))
const integerPart = amountRaw.div(TEN.pow(new BN(decimals))) // 165000 / 10000 = 16
const decimalPart = amountRaw.mod(TEN.pow(new BN(decimals))) // 165000 % 10000 = 5000
const decimalsPadded = decimalPart.toString().padStart(decimals, '0')

return { integerPart, decimalPart, decimalsPadded }
}

interface SmartFormatParams<T> extends Exclude<FormatAmountParams<T>, 'thousandSeparator' | 'isLocaleAware'> {
smallLimit?: number
}

/**
* formatSmart
* @description prettier formatting based on Gnosis Safe - uses same signature as formatAmount
* @param amount
* @param amountPrecision
*/
export function formatSmart(amount: BN, amountPrecision: number): string
export function formatSmart(amount: null | undefined, amountPrecision: number): null
export function formatSmart(params: SmartFormatParams<BN>): string
export function formatSmart(params: SmartFormatParams<null | undefined>): null
export function formatSmart(
params: SmartFormatParams<BN | null | undefined> | BN | null | undefined,
_amountPrecision?: number,
): string | null {
/*
1. integer part in Billion or Trillion becomes abbreviated w/4 fraction digits + decimals are DROPPED
==> e.g 1.2546T
2. everything under is shown as is, with local thousands separator and 4 decimal points
==> e.g 125,456,777.8888
3. anything under "smallLimit" is shown as < ${smallLimit}
==> < 0.0001
*/
let amount: BN
let precision: number

let decimals = DEFAULT_DECIMALS
const smallLimit = DEFAULT_SMALL_LIMIT_AS_WEI

if (!params || ('amount' in params && !params.amount)) {
return null
} else if (BN.isBN(params)) {
amount = params
precision = _amountPrecision as number
} else {
amount = params.amount as BN
precision = params.precision
decimals = params.decimals ?? decimals
}

// amount is already zero
if (amount.isZero()) return amount.toString()

const thousandSymbol = THOUSANDS_SYMBOL
const decimalSymbol = DECIMALS_SYMBOL

const actualDecimals = Math.min(precision, decimals)
const numberParts = _decomposeBn(amount, precision, actualDecimals)

return _formatSmart(numberParts, thousandSymbol, decimalSymbol, smallLimit)
}

interface FormatAmountParams<T> {
Expand Down

0 comments on commit 40ecea3

Please sign in to comment.