169 lines
5.2 KiB
TypeScript
169 lines
5.2 KiB
TypeScript
import { BigNumber } from 'bignumber.js';
|
|
import { BigNumber as EthersBigNumber } from 'ethers';
|
|
import isNil from 'lodash/isNil';
|
|
import memoize from 'lodash/memoize';
|
|
import React from 'react';
|
|
import { getUserLocale } from './utils';
|
|
|
|
const MAX_FRACTION_DIGITS = 20;
|
|
|
|
export function toDecimal(numberOfDecimals: number) {
|
|
return 1 / Math.pow(10, numberOfDecimals);
|
|
}
|
|
|
|
export function toBigNum(
|
|
rawValue: string | number | EthersBigNumber,
|
|
decimals: number
|
|
): BigNumber {
|
|
return new BigNumber(
|
|
rawValue instanceof EthersBigNumber ? rawValue.toString() : rawValue || 0
|
|
).dividedBy(Math.pow(10, decimals));
|
|
}
|
|
|
|
export function addDecimal(
|
|
value: string | number | EthersBigNumber,
|
|
decimals: number,
|
|
decimalPrecision = decimals
|
|
): string {
|
|
if (!decimals) return value.toString();
|
|
if (!decimalPrecision || decimalPrecision < 0) {
|
|
return toBigNum(value, decimals).toFixed(0);
|
|
}
|
|
return toBigNum(value, decimals).toFixed(decimalPrecision);
|
|
}
|
|
|
|
export function removeDecimal(value: string, decimals: number): string {
|
|
if (!decimals) return value;
|
|
return new BigNumber(value || 0).times(Math.pow(10, decimals)).toFixed(0);
|
|
}
|
|
|
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat
|
|
export const getNumberFormat = memoize((digits: number) => {
|
|
if (isNil(digits) || digits < 0) {
|
|
return new Intl.NumberFormat(getUserLocale());
|
|
}
|
|
return new Intl.NumberFormat(getUserLocale(), {
|
|
minimumFractionDigits: Math.min(Math.max(0, digits), MAX_FRACTION_DIGITS),
|
|
maximumFractionDigits: Math.min(Math.max(0, digits), MAX_FRACTION_DIGITS),
|
|
});
|
|
});
|
|
|
|
export const getDecimalSeparator = memoize(
|
|
() =>
|
|
getNumberFormat(1)
|
|
.formatToParts(1.1)
|
|
.find((part) => part.type === 'decimal')?.value
|
|
);
|
|
|
|
export const formatNumber = (
|
|
rawValue: string | number | BigNumber,
|
|
formatDecimals = 0
|
|
) => {
|
|
return getNumberFormat(formatDecimals).format(Number(rawValue));
|
|
};
|
|
|
|
export const addDecimalsFormatNumber = (
|
|
rawValue: string | number,
|
|
decimalPlaces: number,
|
|
formatDecimals: number = decimalPlaces
|
|
) => {
|
|
const x = addDecimal(rawValue, decimalPlaces);
|
|
|
|
return formatNumber(x, formatDecimals);
|
|
};
|
|
|
|
export const formatNumberPercentage = (value: BigNumber, decimals?: number) => {
|
|
const decimalPlaces =
|
|
typeof decimals === 'undefined' ? Math.max(value.dp() || 0, 2) : decimals;
|
|
return `${formatNumber(value, decimalPlaces)}%`;
|
|
};
|
|
|
|
export const toNumberParts = (
|
|
value: BigNumber | null | undefined,
|
|
decimals = 18
|
|
): [integers: string, decimalPlaces: string] => {
|
|
if (!value) {
|
|
return ['0', '0'.repeat(decimals)];
|
|
}
|
|
const separator = getDecimalSeparator() || '.';
|
|
const [integers, decimalsPlaces] = formatNumber(value, decimals)
|
|
.toString()
|
|
.split(separator);
|
|
return [integers, decimalsPlaces || ''];
|
|
};
|
|
|
|
export const useNumberParts = (
|
|
value: BigNumber | null | undefined,
|
|
decimals: number
|
|
): [integers: string, decimalPlaces: string] => {
|
|
return React.useMemo(() => toNumberParts(value, decimals), [decimals, value]);
|
|
};
|
|
|
|
export const isNumeric = (
|
|
value?: string | number | BigNumber | null
|
|
): value is NonNullable<number | string> => /^-?\d*\.?\d+$/.test(String(value));
|
|
|
|
const INFINITY = '∞';
|
|
const DEFAULT_COMPACT_ABOVE = 1_000_000;
|
|
const DEFAULT_COMPACT_CAP = new BigNumber(1e24);
|
|
/**
|
|
* Compacts given number to human readable format.
|
|
* @param number
|
|
* @param decimals Number of decimal places
|
|
* @param compactDisplay Display mode; short -> 1e6 == 1M; ling -> 1e6 1 million
|
|
* @param compactAbove Compact number above threshold
|
|
* @param cap Use scientific notation above threshold
|
|
*/
|
|
export const compactNumber = (
|
|
number: BigNumber,
|
|
decimals: number | 'infer' = 'infer',
|
|
compactDisplay: 'short' | 'long' = 'short',
|
|
compactAbove = DEFAULT_COMPACT_ABOVE,
|
|
cap = DEFAULT_COMPACT_CAP
|
|
) => {
|
|
if (!number.isFinite()) return `${number.isNegative() ? '-' : ''}${INFINITY}`;
|
|
|
|
const decimalPlaces =
|
|
(decimals === 'infer' ? number.decimalPlaces() : decimals) || 0;
|
|
|
|
if (number.isLessThan(DEFAULT_COMPACT_ABOVE)) {
|
|
return formatNumber(number, decimalPlaces);
|
|
}
|
|
|
|
/**
|
|
* Note: it compacts number up to 1_000_000_000_000 (1e12) -> 1T, all above is formatted as iteration of T.
|
|
* Example: 1.579208923731619e59 -> 157,920,892,373,161,900,000,000,000,000,000,000,000,000,000,000T
|
|
*/
|
|
const compactNumFormat = new Intl.NumberFormat(getUserLocale(), {
|
|
minimumFractionDigits: Math.max(0, decimalPlaces),
|
|
maximumFractionDigits: Math.max(0, decimalPlaces),
|
|
notation: 'compact',
|
|
compactDisplay,
|
|
});
|
|
const scientificNumFormat = new Intl.NumberFormat(getUserLocale(), {
|
|
minimumFractionDigits: Math.max(0, decimalPlaces),
|
|
maximumFractionDigits: Math.max(0, decimalPlaces),
|
|
notation: 'scientific',
|
|
});
|
|
|
|
if (number.isGreaterThan(DEFAULT_COMPACT_CAP)) {
|
|
const r = /E(\d+)$/i;
|
|
const formatted = scientificNumFormat.format(Number(number));
|
|
const eNotation = formatted.match(r);
|
|
if (eNotation && eNotation.length > 1) {
|
|
const power = eNotation[1];
|
|
return (
|
|
<span>
|
|
{formatted.replace(r, '')}{' '}
|
|
<span>
|
|
× 10
|
|
<sup>{power}</sup>
|
|
</span>
|
|
</span>
|
|
);
|
|
}
|
|
}
|
|
|
|
return compactNumFormat.format(Number(number));
|
|
};
|