import { BigNumber } from 'bignumber.js'; import numeral from 'numeral'; export const formatBalance = ( originAmount: BigNumber | string | number, showDecimals: number, autoUnit = false, ): string => { const amount = new BigNumber(originAmount); if (autoUnit && amount.gt(1000)) { const fmt = { prefix: '', decimalSeparator: '.', groupSeparator: ',', groupSize: 3, secondaryGroupSize: 0, fractionGroupSeparator: ' ', fractionGroupSize: 0, suffix: '', }; return amount.toFormat(showDecimals, BigNumber.ROUND_DOWN, fmt); } return amount.toFormat(showDecimals, BigNumber.ROUND_DOWN); }; type Props = { input: BigNumber | number | string | undefined; showDecimals?: number; showIntegerOnly?: boolean; showDecimalsOnly?: boolean; showPrecisionDecimals?: number; /** 超过多少位之后才使用科学计数法,默认8位小数,某些地方需要显示全部小数,可以传入18 */ exponentialDecimalsAmount?: number; noGroupSeparator?: boolean; /** dp 的第二位参数,决定多少位舍去小数. 不传则为 1 */ roundingMode?: BigNumber.RoundingMode; }; /** * format to readable number, like: 0.00 -> 0, 1.00 -> 1, 1.235 -> 1.23, 1.230 -> 1.23 * @param input */ export function formatReadableNumber({ input, showDecimals = 4, showPrecisionDecimals = 2, exponentialDecimalsAmount = 8, showIntegerOnly = false, showDecimalsOnly = false, noGroupSeparator = false, roundingMode = BigNumber.ROUND_DOWN, }: Props): string { if (!input) return '-'; const source = new BigNumber(input); if (source.isNaN()) { return '-'; } let amount = source.dp(showDecimals, roundingMode); if (amount.eq(0) && (source.gt(0) || source.lt(0))) { const significantDigits = showPrecisionDecimals ?? showDecimals; amount = source.sd(significantDigits, BigNumber.ROUND_DOWN); // https://www.notion.so/dodotopia/2-0-8-fcc139e3679a46c5b78e08ce51733118?pvs=4 // 至少显示2位有效数字,但若0数位大于8位,使用科学计数法 if ( amount.dp() > exponentialDecimalsAmount && !showDecimalsOnly && !showIntegerOnly ) { return amount.toExponential(); } } if (showIntegerOnly) { amount = amount.integerValue(BigNumber.ROUND_DOWN); } if (showDecimalsOnly) { amount = amount.minus(amount.integerValue(BigNumber.ROUND_DOWN)); } if (noGroupSeparator) { return amount.toFormat({ decimalSeparator: '.', groupSeparator: '', groupSize: 3, secondaryGroupSize: 0, fractionGroupSeparator: '', fractionGroupSize: 0, }); } return amount.toFormat(); } /** * format to percentage number * @param param0 input number */ export function formatPercentageNumber({ input, showDecimals = 2, /** 这里百分比默认用四舍五入 */ roundingMode = BigNumber.ROUND_HALF_UP, }: { input?: BigNumber | string | number | null; showDecimals?: number; roundingMode?: BigNumber.RoundingMode; }): string { if (input === null || input === undefined) { return '-'; } return `${formatReadableNumber({ input: new BigNumber(input || 0).multipliedBy(100), showDecimals, roundingMode, })}%`; } export function formatTokenAmountNumber({ input, decimals, showPrecisionDecimals = 2, exponentialDecimalsAmount, noGroupSeparator, }: { input?: BigNumber | number | string | null; decimals?: number; } & Pick< Props, 'exponentialDecimalsAmount' | 'showPrecisionDecimals' | 'noGroupSeparator' >): string { if (input === undefined || input === null) { return '-'; } const source = new BigNumber(input); if (source.isNaN()) { return '-'; } const showDecimals = // eslint-disable-next-line no-nested-ternary decimals === undefined ? 0 : decimals > 6 ? 6 : decimals > 4 ? 4 : decimals; return formatReadableNumber({ input: source, showDecimals, showPrecisionDecimals, exponentialDecimalsAmount, noGroupSeparator, }); } const kilo = 1000; const million = 1000000; const billion = 1000000000; function getNegative(num: number) { return new BigNumber(num).negated(); } /** * format to short number, like: -0.12 -> 0, 0.0000123->0.000012, 123.234 -> 123.23, 1234.12 -> 1.23K, 1000000.123->1.00M * @param n */ export function formatShortNumber(n?: BigNumber, showDecimals = 4): string { if (!n || n.isNaN()) { return '-'; } if (n.eq(0)) { return '0'; } if (n.lte(0.000001) && n.gte(-0.000001)) { return n.toExponential(2); } if (n.lt(1) && n.gt(-1)) { return formatReadableNumber({ input: n, showDecimals }); } if (n.lt(kilo) && n.gt(getNegative(kilo))) { return formatReadableNumber({ input: n, showDecimals }); } if (n.lt(million) && n.gt(getNegative(million))) { return `${formatReadableNumber({ input: n.div(kilo), showDecimals: 2 })}K`; } return `${formatReadableNumber({ input: n.div(million), showDecimals: 2, })}M`; } /** * 小于一千 两位小数,大于等于一千四位小数 * @param n */ export function formatShortNumberV2(n?: BigNumber): string { if (!n || n.isNaN()) { return '-'; } if (n.eq(0)) { return '0'; } if (n.lte(0.000001) && n.gte(-0.000001)) { return n.toExponential(2); } if (n.lt(1) && n.gt(-1)) { return formatReadableNumber({ input: n, showDecimals: 2 }); } if (n.lt(kilo) && n.gt(getNegative(kilo))) { return formatReadableNumber({ input: n, showDecimals: 2 }); } if (n.lt(million) && n.gt(getNegative(million))) { return `${formatReadableNumber({ input: n.div(kilo), showDecimals: 4 })}K`; } return `${formatReadableNumber({ input: n.div(million), showDecimals: 4, })}M`; } /** * 格式化大数字 */ export function formatBigNumber({ input, showDecimals = 4, useFormatShortNumberV2 = false, }: { input?: BigNumber | number | string | null; showDecimals?: number; useFormatShortNumberV2?: boolean; }): string { if (input === null || input === undefined) { return '-'; } const source = new BigNumber(input); if (source.isNaN()) { return '-'; } if (source.lte(billion) && source.gt(getNegative(billion))) { if (useFormatShortNumberV2) { return formatShortNumberV2(source); } return formatShortNumber(source, showDecimals); } return `${formatReadableNumber({ input: source.div(billion), showDecimals, })}B`; } /** * 超过 billion 的数字使用科学计数法 * 19971400000000=1.99714×10^13 */ export function formatExponentialNotation(n?: BigNumber) { if (!n || n.isNaN()) { return '-'; } if (n.isZero()) { return '0'; } if (n.lte(billion) && n.gt(getNegative(billion))) { return formatShortNumber(n); } const n1 = n.toExponential(2); if (n1.includes('e+')) { const [a1, b1] = n1.split('e+'); if (a1 && b1) { return `${a1}x10^${b1}`; } } return n1; } export const toWei = ( amount: BigNumber | string | number, decimals: number, notDp?: boolean, ) => { const result = new BigNumber(amount).times(new BigNumber(10).pow(decimals)); if (notDp) { return result; } return result.dp(0); }; export const byWei = ( amount: BigNumber | string | number, decimals: number, ) => { return new BigNumber(amount).div(new BigNumber(10).pow(decimals)); }; const isZero = (obj: string | number | BigNumber): boolean => { return new BigNumber(obj).isZero(); }; function toFixed(num: BigNumber | number): string { if (typeof num === 'number') return toFixed(new BigNumber(num)); if (num.isNaN()) return 'NaN'; if (num.isNegative()) return `-${toFixed(num.negated())}`; if (num.isZero()) return '0'; let int = num.integerValue(BigNumber.ROUND_FLOOR); let dec = num.minus(int); // Int part let str = ''; while (!int.isZero()) { const last = int.modulo(10).toString(); str = last.toString() + str; int = int.idiv(10); } if (str === '') str = '0'; // decimal part if (dec.isZero()) return str; str += '.'; let cnt = 0; // Meow: magic number 100 here for avoiding infinite loop // On certain BigNumber impls which uses non 10-based repr, 10-based decimal string may not terminate finitely // Hence we are dropping everything > 100 digits after the decimal point, regardless of rounding mode (effectively rounding to -Infinity) while (!dec.isZero() && cnt < 100) { const expanded = dec.times(10); const intval = expanded.integerValue(BigNumber.ROUND_FLOOR); str += intval.toString(); dec = expanded.minus(intval); cnt += 1; } return str; } export const fixedString = ( raw: string | number | BigNumber, showDecimals?: number, ): string => { if (typeof raw === 'string' && !raw.match(/[-+]?\d+(.+d+)?/)) return raw; // console.log('Fixing', raw); const bn = new BigNumber(raw); if (showDecimals) { let result = bn.toFixed(showDecimals, BigNumber.ROUND_DOWN); if (isZero(result)) { result = toFixed(bn.precision(showDecimals, BigNumber.ROUND_DOWN)); } return result; } return toFixed(bn); }; /** * * @param input * @param limit * @returns * @deprecated 该方法可能会返回 0.000000 的结果,尽量使用 formatTokenAmountNumber 替代 */ export function clampDecimal(input: string, limit: number): string { const splitted = input.split('.'); if (splitted.length === 1) return input; const [seg1, seg2] = splitted; return `${seg1}.${seg2.substr(0, limit)}`; } export const fixedInputStringToFormattedNumber = ( inputValue: string, inputTokenShowDecimals: number, ): string | null => { let inputString = inputValue; const regexFixedStrings = inputString.match('[0-9。.]+'); if (regexFixedStrings && regexFixedStrings.length > 0) { // eslint-disable-next-line prefer-destructuring inputString = regexFixedStrings[0]; } else { inputString = ''; } inputString = inputString.replace('。', '.'); let decimalsCount = 0; if (inputString) { const inputSplits = inputString.split('.'); const fixedInputSplits = inputSplits[0].length === 0 ? ['0'] : [inputSplits[0]]; if (inputSplits.length > 1) { fixedInputSplits[1] = inputSplits.slice(1, inputSplits.length).join(''); } else if ( fixedInputSplits[0].length > 1 && fixedInputSplits[0][0] === '0' ) { fixedInputSplits[0] = fixedInputSplits[0].slice( 1, fixedInputSplits[0].length, ); } if (fixedInputSplits.length > 1) { decimalsCount = fixedInputSplits[1].length; } inputString = fixedInputSplits.join('.'); } if (inputTokenShowDecimals >= 0 && decimalsCount > inputTokenShowDecimals) { return fixedString(inputString, inputTokenShowDecimals); } return inputString; }; export const formatApy = ( amount: BigNumber, showDecimals = 4, notPercent?: boolean, zipMinNumber = 1000, ): string => { const amountPercent = amount.times(100); const numeralFormatStr = amountPercent.lt(zipMinNumber) ? '0.00' : '0.00a'; return ( numeral(amountPercent.toFormat(showDecimals, BigNumber.ROUND_DOWN)).format( numeralFormatStr, ) + (notPercent ? '' : '%') ); };