From a21feea6994e830d78217370e13de656c6c4d8b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20G=C5=82ownia?= Date: Fri, 9 Feb 2024 11:31:15 +0100 Subject: [PATCH] chore(utils): improve formatNumber to keep precision (#5761) --- .../referrals/hooks/use-referral-program.ts | 6 +- .../referrals/referral-statistics.tsx | 8 +- .../competitions/competitions-leaderboard.tsx | 5 +- libs/liquidity/src/lib/liquidity-table.tsx | 12 +-- libs/market-depth/src/lib/depth-chart.tsx | 7 +- libs/utils/src/lib/format/number.spec.ts | 26 +----- libs/utils/src/lib/format/number.ts | 92 ++++++++++++------- libs/utils/src/lib/format/range.spec.ts | 24 ++--- 8 files changed, 92 insertions(+), 88 deletions(-) diff --git a/apps/trading/client-pages/referrals/hooks/use-referral-program.ts b/apps/trading/client-pages/referrals/hooks/use-referral-program.ts index e530d6f05..dffb01a2c 100644 --- a/apps/trading/client-pages/referrals/hooks/use-referral-program.ts +++ b/apps/trading/client-pages/referrals/hooks/use-referral-program.ts @@ -1,4 +1,4 @@ -import { getNumberFormat } from '@vegaprotocol/utils'; +import { formatNumber } from '@vegaprotocol/utils'; import sortBy from 'lodash/sortBy'; import omit from 'lodash/omit'; import { useReferralProgramQuery } from './__generated__/CurrentReferralProgram'; @@ -107,9 +107,7 @@ export const useReferralProgram = () => { discountFactor: Number(t.referralDiscountFactor), discount: BigNumber(t.referralDiscountFactor).times(100).toFixed(2) + '%', minimumVolume: Number(t.minimumRunningNotionalTakerVolume), - volume: getNumberFormat(0).format( - Number(t.minimumRunningNotionalTakerVolume) - ), + volume: formatNumber(t.minimumRunningNotionalTakerVolume, 0), epochs: Number(t.minimumEpochs), }; }); diff --git a/apps/trading/client-pages/referrals/referral-statistics.tsx b/apps/trading/client-pages/referrals/referral-statistics.tsx index 245871d8e..38cc15b5b 100644 --- a/apps/trading/client-pages/referrals/referral-statistics.tsx +++ b/apps/trading/client-pages/referrals/referral-statistics.tsx @@ -14,9 +14,9 @@ import { import { useVegaWallet } from '@vegaprotocol/wallet'; import { addDecimalsFormatNumber, + formatNumber, getDateFormat, getDateTimeFormat, - getNumberFormat, getUserLocale, removePaginationWrapper, } from '@vegaprotocol/utils'; @@ -323,7 +323,7 @@ export const Statistics = ({ } description={} > - {getNumberFormat(0).format(Number(totalCommissionValue))} + {formatNumber(totalCommissionValue, 0)} ); @@ -563,8 +563,8 @@ export const RefereesTable = ({ ) .map((r) => ({ ...r, - volume: getNumberFormat(0).format(r.volume), - commission: getNumberFormat(0).format(r.commission), + volume: formatNumber(r.volume, 0), + commission: formatNumber(r.commission, 0), })) .reverse()} /> diff --git a/apps/trading/components/competitions/competitions-leaderboard.tsx b/apps/trading/components/competitions/competitions-leaderboard.tsx index 40e87d39b..5a3291cc3 100644 --- a/apps/trading/components/competitions/competitions-leaderboard.tsx +++ b/apps/trading/components/competitions/competitions-leaderboard.tsx @@ -1,6 +1,6 @@ import { Link } from 'react-router-dom'; import { Splash } from '@vegaprotocol/ui-toolkit'; -import { getNumberFormat } from '@vegaprotocol/utils'; +import { formatNumber } from '@vegaprotocol/utils'; import { type useTeams } from '../../lib/hooks/use-teams'; import { useT } from '../../lib/use-t'; import { Table } from '../table'; @@ -15,8 +15,7 @@ export const CompetitionsLeaderboard = ({ }) => { const t = useT(); - const num = (n?: number | string) => - !n ? '-' : getNumberFormat(0).format(Number(n)); + const num = (n?: number | string) => (!n ? '-' : formatNumber(n, 0)); if (!data || data.length === 0) { return {t('Could not find any teams')}; diff --git a/libs/liquidity/src/lib/liquidity-table.tsx b/libs/liquidity/src/lib/liquidity-table.tsx index 523297efb..db3b4dd44 100644 --- a/libs/liquidity/src/lib/liquidity-table.tsx +++ b/libs/liquidity/src/lib/liquidity-table.tsx @@ -94,7 +94,7 @@ export const LiquidityTable = ({ return `${addDecimalsFormatNumberQuantum( value, assetDecimalPlaces ?? 0, - quantum ?? 0 + quantum ?? 1 )}`; }; @@ -165,7 +165,7 @@ export const LiquidityTable = ({ return `${addDecimalsFormatNumberQuantum( newValue, assetDecimalPlaces ?? 0, - quantum ?? 0 + quantum ?? 1 )}`; }; @@ -227,7 +227,7 @@ export const LiquidityTable = ({ addDecimalsFormatNumberQuantum( pendingCommitmentAmount, assetDecimalPlaces ?? 0, - quantum ?? 0 + quantum ?? 1 ); if ( @@ -238,7 +238,7 @@ export const LiquidityTable = ({ addDecimalsFormatNumberQuantum( currentCommitmentAmount, assetDecimalPlaces ?? 0, - quantum ?? 0 + quantum ?? 1 ); return ( @@ -286,7 +286,7 @@ export const LiquidityTable = ({ addDecimalsFormatNumberQuantum( pendingCommitmentAmount, assetDecimalPlaces ?? 0, - quantum ?? 0 + quantum ?? 1 ); if ( @@ -297,7 +297,7 @@ export const LiquidityTable = ({ addDecimalsFormatNumberQuantum( currentCommitmentAmount, assetDecimalPlaces ?? 0, - quantum ?? 0 + quantum ?? 1 ); return ( diff --git a/libs/market-depth/src/lib/depth-chart.tsx b/libs/market-depth/src/lib/depth-chart.tsx index 0b7cc79ea..c370e06aa 100644 --- a/libs/market-depth/src/lib/depth-chart.tsx +++ b/libs/market-depth/src/lib/depth-chart.tsx @@ -1,7 +1,7 @@ import { DepthChart } from 'pennant'; import throttle from 'lodash/throttle'; import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; -import { addDecimal, getNumberFormat } from '@vegaprotocol/utils'; +import { addDecimal, formatNumber } from '@vegaprotocol/utils'; import { useThemeSwitcher } from '@vegaprotocol/react-helpers'; import { useDataProvider } from '@vegaprotocol/data-provider'; import { marketDepthProvider } from './market-depth-provider'; @@ -216,13 +216,12 @@ export const DepthChartContainer = ({ marketId }: DepthChartManagerProps) => { const volumeFormat = useCallback( (volume: number) => - getNumberFormat(market?.positionDecimalPlaces || 0).format(volume), + formatNumber(volume, market?.positionDecimalPlaces || 0), [market?.positionDecimalPlaces] ); const priceFormat = useCallback( - (price: number) => - getNumberFormat(market?.decimalPlaces || 0).format(price), + (price: number) => formatNumber(price, market?.decimalPlaces || 0), [market?.decimalPlaces] ); diff --git a/libs/utils/src/lib/format/number.spec.ts b/libs/utils/src/lib/format/number.spec.ts index 121d00547..7e1ace053 100644 --- a/libs/utils/src/lib/format/number.spec.ts +++ b/libs/utils/src/lib/format/number.spec.ts @@ -23,6 +23,7 @@ describe('number utils', () => { { v: new BigNumber(123000), d: 1, o: '12,300.0' }, { v: new BigNumber(123001), d: 2, o: '1,230.01' }, { v: new BigNumber(123001000), d: 2, o: '1,230,010.00' }, + { v: '100000000000000000001', d: 18, o: '100.000000000000000001' }, ])( 'formats with addDecimalsFormatNumber given number correctly', ({ v, d, o }) => { @@ -31,27 +32,10 @@ describe('number utils', () => { ); it.each([ - { v: new BigNumber(123000), d: 5, o: '1.23', q: 0.1 }, - { v: new BigNumber(123000), d: 3, o: '123.00', q: 0.1 }, - { v: new BigNumber(123000), d: 1, o: '12,300.00', q: 0.1 }, - { v: new BigNumber(123001000), d: 2, o: '1,230,010.00', q: 0.1 }, - { v: new BigNumber(123001), d: 2, o: '1,230.01', q: 100 }, - { v: new BigNumber(123001), d: 2, o: '1,230.01', q: 0.1 }, - { v: new BigNumber(123001), d: 2, o: '1,230.01', q: 1 }, - { - v: BigNumber('123456789123456789'), - d: 10, - o: '12,345,678.91234568', - q: '0.00003846', - }, - { - v: BigNumber('123456789123456789'), - d: 10, - o: '12,345,678.91234568', - q: '1', - }, - // USDT / USDC - { v: new BigNumber(12345678), d: 6, o: '12.35', q: 1000000 }, + { v: '1234000000000000000', d: 18, q: '1000000000000000000', o: '1.23' }, //vega + { v: '1235000000000000000', d: 18, q: '1000000000000000000', o: '1.24' }, //vega + { v: '1230012', d: 6, q: '1000000', o: '1.23' }, // USDT + { v: '1234560000000000000', d: 18, q: '500000000000000', o: '1.2346' }, // WEth ])( 'formats with addDecimalsFormatNumberQuantum given number correctly', ({ v, d, o, q }) => { diff --git a/libs/utils/src/lib/format/number.ts b/libs/utils/src/lib/format/number.ts index 90d41974f..7d407e050 100644 --- a/libs/utils/src/lib/format/number.ts +++ b/libs/utils/src/lib/format/number.ts @@ -1,5 +1,4 @@ import { BigNumber } from 'bignumber.js'; -import isNil from 'lodash/isNil'; import memoize from 'lodash/memoize'; import { getUserLocale } from '../get-user-locale'; @@ -53,36 +52,36 @@ export function removeDecimal( return new BigNumber(value || 0).times(times).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), MIN_FRACTION_DIGITS), - maximumFractionDigits: Math.min(Math.max(0, digits), MAX_FRACTION_DIGITS), - }); -}); - -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat -export const getFixedNumberFormat = 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) + new Intl.NumberFormat(getUserLocale()) .formatToParts(1.1) - .find((part) => part.type === 'decimal')?.value + .find((part) => part.type === 'decimal')?.value ?? '.' ); -/** formatNumber will format the number with fixed decimals +export const getGroupFormat = memoize(() => { + const parts = new Intl.NumberFormat(getUserLocale()).formatToParts( + 100000000000.1 + ); + const groupSeparator = parts.find((part) => part.type === 'group')?.value; + const groupSize = + (groupSeparator && + parts.reverse().find((part) => part.type === 'integer')?.value.length) || + 0; + return { + groupSize, + groupSeparator, + }; +}); + +const getFormat = memoize(() => ({ + decimalSeparator: getDecimalSeparator(), + ...getGroupFormat(), +})); + +/** + * formatNumber will format the number with maximum number of decimals + * trailing zeros are removed but min(MIN_FRACTION_DIGITS, formatDecimals) decimal places will be kept * @param rawValue - should be a number that is not outside the safe range fail as in https://mikemcl.github.io/bignumber.js/#toN * @param formatDecimals - number of decimals to use */ @@ -90,7 +89,23 @@ export const formatNumber = ( rawValue: string | number | BigNumber, formatDecimals = 0 ) => { - return getNumberFormat(formatDecimals).format(Number(rawValue)); + const decimalPlaces = Math.min( + Math.max(0, formatDecimals), + MAX_FRACTION_DIGITS + ); + const format = getFormat(); + const formatted = new BigNumber(rawValue).toFormat(decimalPlaces, format); + // if there are no decimal places just return formatted value + if (!decimalPlaces) { + return formatted; + } + // minimum number of decimal places to keep when removing trailing zeros + const minimumFractionDigits = Math.min(decimalPlaces, MIN_FRACTION_DIGITS); + const parts = formatted.split(format.decimalSeparator); + parts[1] = (parts[1] || '') + .replace(/0+$/, '') + .padEnd(minimumFractionDigits, '0'); + return parts.join(format.decimalSeparator); }; /** formatNumberFixed will format the number with fixed decimals @@ -101,7 +116,10 @@ export const formatNumberFixed = ( rawValue: string | number | BigNumber, formatDecimals = 0 ) => { - return getFixedNumberFormat(formatDecimals).format(Number(rawValue)); + return new BigNumber(rawValue).toFormat( + Math.min(Math.max(0, formatDecimals), MAX_FRACTION_DIGITS), + getFormat() + ); }; export const quantumDecimalPlaces = ( @@ -131,9 +149,14 @@ export const addDecimalsFormatNumberQuantum = ( if (isNaN(Number(quantum))) { return addDecimalsFormatNumber(rawValue, decimalPlaces); } - const quantumValue = addDecimal(quantum, decimalPlaces); - const numberDP = Math.max(0, Math.log10(100 / Number(quantumValue))); - return addDecimalsFormatNumber(rawValue, decimalPlaces, Math.ceil(numberDP)); + const numberDP = Math.ceil( + Math.abs(Math.log10(toBigNum(quantum, decimalPlaces).toNumber())) + ); + return addDecimalsFormatNumber( + rawValue, + decimalPlaces, + Math.max(MIN_FRACTION_DIGITS, numberDP) + ); }; export const addDecimalsFormatNumber = ( @@ -141,9 +164,10 @@ export const addDecimalsFormatNumber = ( decimalPlaces: number, formatDecimals: number = decimalPlaces ) => { - const x = addDecimal(rawValue, decimalPlaces); - - return formatNumber(x, formatDecimals); + return formatNumber( + new BigNumber(rawValue || 0).dividedBy(Math.pow(10, decimalPlaces)), + formatDecimals + ); }; export const addDecimalsFixedFormatNumber = ( diff --git a/libs/utils/src/lib/format/range.spec.ts b/libs/utils/src/lib/format/range.spec.ts index 5730c2fbc..5af45f802 100644 --- a/libs/utils/src/lib/format/range.spec.ts +++ b/libs/utils/src/lib/format/range.spec.ts @@ -10,24 +10,24 @@ describe('formatValue', () => { { v: '123456789123456789', d: 10, - o: '12,345,678.91234568', + o: '12,345,678.9123456789', }, ])('formats values correctly', ({ v, d, o }) => { expect(formatValue(v, d)).toStrictEqual(o); }); it.each([ - { v: 123000, d: 5, o: '1.23', q: '0.1' }, - { v: 123000, d: 3, o: '123.00', q: '0.1' }, - { v: 123000, d: 1, o: '12,300.00', q: '0.1' }, - { v: 123001000, d: 2, o: '1,230,010.00', q: '0.1' }, + { v: 123000, d: 5, o: '1.23', q: '1' }, + { v: 123000, d: 3, o: '123.00', q: '1' }, + { v: 123000, d: 1, o: '12,300.00', q: '1' }, + { v: 123001000, d: 2, o: '1,230,010.00', q: '1' }, { v: 123001, d: 2, o: '1,230.01', q: '100' }, - { v: 123001, d: 2, o: '1,230.01', q: '0.1' }, + { v: 123001, d: 2, o: '1,230.01', q: '1' }, { v: '123456789123456789', d: 10, - o: '12,345,678.91234568', - q: '0.00003846', + o: '12,345,678.91235', + q: '384600', }, ])( 'formats with formatValue with quantum given number correctly', @@ -42,15 +42,15 @@ describe('formatRange', () => { min: 123000, max: 12300011111, d: 5, - o: '1.23 - 123,000.11111', - q: '0.1', + o: '1.23 - 123,000.11', + q: '1000', }, { min: 123000, max: 12300011111, d: 3, - o: '123.00 - 12,300,011.111', - q: '0.1', + o: '123.00 - 12,300,011.11', + q: '100', }, { min: 123000,