From 8fc599380f161ced04545c98cc4680afe7504fa1 Mon Sep 17 00:00:00 2001 From: Nam Nguyen Date: Thu, 26 Oct 2023 13:39:17 +0700 Subject: [PATCH] fix: number parsing (#2329) --- .github/workflows/ci.yaml | 8 +-- .github/workflows/cypress.yml | 20 +++--- .github/workflows/pr.yaml | 8 +-- .github/workflows/release.yaml | 4 +- .github/workflows/schedule.yml | 4 +- package.json | 6 +- .../InboxItemKyberAIWatchList.tsx | 7 +- src/components/ProAmm/CandleStickChart.tsx | 3 +- src/components/YieldPools/ListItem.tsx | 6 +- .../LimitOrder/useValidateInputError.tsx | 2 +- .../TotalEarningsAndChainSelect.tsx | 2 +- src/types/intl.d.ts | 5 ++ src/utils/dmm.ts | 2 +- src/utils/numbers.ts | 72 +++++++++---------- yarn.lock | 17 +++-- 15 files changed, 94 insertions(+), 72 deletions(-) create mode 100644 src/types/intl.d.ts diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index cd4eff2f0b..26e8d1ab17 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -71,10 +71,10 @@ jobs: - name: Checkout uses: actions/checkout@v2 - - name: Set up Node.js 18.15.0 + - name: Set up Node.js uses: actions/setup-node@v1 with: - node-version: 18.15.0 + node-version: 20.9.0 registry-url: 'https://npm.pkg.github.com' scope: '@kybernetwork' @@ -147,10 +147,10 @@ jobs: - name: Checkout uses: actions/checkout@v2 - - name: Set up Node.js 18.15.0 + - name: Set up Node.js uses: actions/setup-node@v1 with: - node-version: 18.15.0 + node-version: 20.9.0 registry-url: 'https://npm.pkg.github.com' scope: '@kybernetwork' diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml index 170fa2a8d9..a1e9cf6fc0 100644 --- a/.github/workflows/cypress.yml +++ b/.github/workflows/cypress.yml @@ -70,10 +70,10 @@ jobs: - name: Check out Git repository uses: actions/checkout@v3 - - name: Set up Node.js 18.15.0 + - name: Set up Node.js uses: actions/setup-node@v3 with: - node-version: 18.15.0 + node-version: 20.9.0 registry-url: 'https://npm.pkg.github.com' scope: '@kybernetwork' @@ -84,9 +84,9 @@ jobs: - name: Install linux deps run: | - sudo apt-get install --no-install-recommends -y \ - fluxbox \ - xvfb + sudo apt-get install --no-install-recommends -y \ + fluxbox \ + xvfb - name: Yarn Build env: @@ -118,14 +118,14 @@ jobs: yarn preview & yarn test-e2e -c baseUrl='http://127.0.0.1:4173/' -e grepTags=smoke,NETWORK=Ethereum env: - DISPLAY: :0.0 + DISPLAY: :0.0 - name: Archive e2e artifacts uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 if: always() with: - name: e2e-artifacts - path: | - cypress/videos - cypress/screenshots + name: e2e-artifacts + path: | + cypress/videos + cypress/screenshots continue-on-error: true diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index cabf110ec8..946aefb8ef 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -70,10 +70,10 @@ jobs: - name: Checkout uses: actions/checkout@v2 - - name: Set up Node.js 18.15.0 + - name: Set up Node.js uses: actions/setup-node@v1 with: - node-version: 18.15.0 + node-version: 20.9.0 registry-url: 'https://npm.pkg.github.com' scope: '@kybernetwork' @@ -144,10 +144,10 @@ jobs: - name: Check out Git repository uses: actions/checkout@v2 - - name: Set up Node.js 18.15.0 + - name: Set up Node.js uses: actions/setup-node@v1 with: - node-version: 18.15.0 + node-version: 20.9.0 registry-url: 'https://npm.pkg.github.com' scope: '@kybernetwork' diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 4fc695a362..a96e6244d1 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -38,10 +38,10 @@ jobs: - name: Checkout uses: actions/checkout@v2 - - name: Set up Node.js 18.15.0 + - name: Set up Node.js uses: actions/setup-node@v1 with: - node-version: 18.15.0 + node-version: 20.9.0 registry-url: 'https://npm.pkg.github.com' scope: '@kybernetwork' diff --git a/.github/workflows/schedule.yml b/.github/workflows/schedule.yml index ad37319e3d..fd159f85b8 100644 --- a/.github/workflows/schedule.yml +++ b/.github/workflows/schedule.yml @@ -17,10 +17,10 @@ jobs: - name: Trigger Code Checkout uses: actions/checkout@v3 - - name: Set up Node.js 18.15.0 + - name: Set up Node.js uses: actions/setup-node@v3 with: - node-version: 18.15.0 + node-version: 20.9.0 registry-url: 'https://npm.pkg.github.com' scope: '@kybernetwork' diff --git a/package.json b/package.json index 67d7d6bdbe..20d3bc1ac8 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "homepage": "/", "license": "GPL-3.0-or-later", "engines": { - "node": "~18.15.0", + "node": "~20.9.0", "yarn": ">=1.22" }, "packageManager": "yarn@1.22.19", @@ -158,7 +158,7 @@ "@types/dompurify": "^3.0.3", "@types/mixpanel-browser": "^2.38.0", "@types/multicodec": "^1.0.0", - "@types/node": "^13.13.52", + "@types/node": "^20.8.8", "@types/numeral": "^2.0.0", "@types/react": "^18.0.15", "@types/react-dom": "^18.0.8", @@ -208,4 +208,4 @@ "@lingui/core": "3.14.0", "@lingui/conf": "3.16.0" } -} +} \ No newline at end of file diff --git a/src/components/Announcement/PrivateAnnoucement/InboxItemKyberAIWatchList.tsx b/src/components/Announcement/PrivateAnnoucement/InboxItemKyberAIWatchList.tsx index f7696b09be..e675629cf2 100644 --- a/src/components/Announcement/PrivateAnnoucement/InboxItemKyberAIWatchList.tsx +++ b/src/components/Announcement/PrivateAnnoucement/InboxItemKyberAIWatchList.tsx @@ -48,7 +48,12 @@ export const TokenInfo = ({ {' '} 0 ? theme.apr : theme.red}> ({+priceChange > 0 && '+'} - {formatDisplayNumber(+priceChange / 100, { style: 'percent', fractionDigits: 2, allowNegative: true })}) + {formatDisplayNumber(+priceChange / 100, { + style: 'percent', + fractionDigits: 2, + allowDisplayNegative: true, + })} + ) )} diff --git a/src/components/ProAmm/CandleStickChart.tsx b/src/components/ProAmm/CandleStickChart.tsx index 3bc8726ef2..a5ba9bfc4f 100644 --- a/src/components/ProAmm/CandleStickChart.tsx +++ b/src/components/ProAmm/CandleStickChart.tsx @@ -125,7 +125,8 @@ const CandleStickChart = ({ borderColor: 'rgba(197, 203, 206, 0.8)', }, localization: { - priceFormatter: (val: number) => formatDisplayNumber(val, { significantDigits: 6, allowNegative: true }), + priceFormatter: (val: number) => + formatDisplayNumber(val, { significantDigits: 6, allowDisplayNegative: true }), }, }) diff --git a/src/components/YieldPools/ListItem.tsx b/src/components/YieldPools/ListItem.tsx index 19352a890d..056304d8eb 100644 --- a/src/components/YieldPools/ListItem.tsx +++ b/src/components/YieldPools/ListItem.tsx @@ -577,7 +577,11 @@ const ListItem = ({ farm }: ListItemProps) => { - {formatDisplayNumber(userStakedBalanceUSD, { style: 'currency', significantDigits: 6, allowZero: false })} + {formatDisplayNumber(userStakedBalanceUSD, { + style: 'currency', + significantDigits: 6, + allowDisplayZero: false, + })} diff --git a/src/components/swapv2/LimitOrder/useValidateInputError.tsx b/src/components/swapv2/LimitOrder/useValidateInputError.tsx index 3a158ebb26..c02d0e8923 100644 --- a/src/components/swapv2/LimitOrder/useValidateInputError.tsx +++ b/src/components/swapv2/LimitOrder/useValidateInputError.tsx @@ -48,7 +48,7 @@ const useValidateInputError = ({ const formatNum = formatDisplayNumber(remainBalance, { style: 'decimal', fractionDigits: 6, - allowNegative: true, + allowDisplayNegative: true, }) return ( diff --git a/src/pages/MyEarnings/TotalEarningsAndChainSelect.tsx b/src/pages/MyEarnings/TotalEarningsAndChainSelect.tsx index 764c1cee8d..dc5fdc6809 100644 --- a/src/pages/MyEarnings/TotalEarningsAndChainSelect.tsx +++ b/src/pages/MyEarnings/TotalEarningsAndChainSelect.tsx @@ -143,7 +143,7 @@ const TotalEarningsAndChainSelect: React.FC = ({ totalEarningToday, total style: 'percent', fractionDigits: 2, significantDigits: 6, - allowNegative: true, + allowDisplayNegative: true, }) : '' diff --git a/src/types/intl.d.ts b/src/types/intl.d.ts new file mode 100644 index 0000000000..2bcdf5ecf5 --- /dev/null +++ b/src/types/intl.d.ts @@ -0,0 +1,5 @@ +declare namespace Intl { + interface NumberFormatOptions { + roundingPriority?: 'auto' | 'morePrecision' | 'lessPrecision' + } +} diff --git a/src/utils/dmm.ts b/src/utils/dmm.ts index f9884b5b31..d37f500246 100644 --- a/src/utils/dmm.ts +++ b/src/utils/dmm.ts @@ -251,7 +251,7 @@ export const getMyLiquidity = ( (parseFloat(liquidityPosition.liquidityTokenBalance) * parseFloat(liquidityPosition.pool.reserveUSD)) / parseFloat(liquidityPosition.pool.totalSupply) - return formatDisplayNumber(myLiquidity, { style: 'currency', significantDigits: 4, allowZero: false }) + return formatDisplayNumber(myLiquidity, { style: 'currency', significantDigits: 4, allowDisplayZero: false }) } function useFarmRewardsPerTimeUnit(farm?: Farm): RewardPerTimeUnit[] { diff --git a/src/utils/numbers.ts b/src/utils/numbers.ts index e50be0019c..abfc09f831 100644 --- a/src/utils/numbers.ts +++ b/src/utils/numbers.ts @@ -67,28 +67,40 @@ const subscriptMap: { [key: string]: string } = { } const log10 = (n: Fraction): number => { - const parsedN = Number(n.toSignificant(30)) + const parsedN = Number(n.toFixed(100)) return Math.log10(parsedN) } -// - $ 123,456,222,333.44444 e+22 eur5 -const regex = /^\s*?\+?(-)?\s*?(\$)?\s*?([\d,]+)(?:\.(\d+))?\s*?(?:e\+?(\-?\d+))?\s*?(%|\w+?)?\s*?$/ -const parseNumPart = (str: string): [string, string, string, string, string, string] => { + +const unitMapping: { [key: string]: string } = { + k: '3', + m: '6', + b: '9', + t: '12', +} +// - $ 123,456,222,333.44444K e+22 eur5 +const regex = /^\s*?\+?(-)?\s*?(\$)?\s*?([\d,]+)(?:\.(\d+))?(\s*?(?:K|M|T))?(?:\s*?e\+?(\-?\d+))?\s*?(%|\w+?)?\s*?$/ +const parseNumPart = (str: string): [string, string, string, string, string, string, string] => { const parsedResult = regex.exec(str) if (parsedResult) { - const [, negative, currency, integer, decimal, exponent, unit] = parsedResult - return [negative || '', currency || '', integer, decimal || '', exponent, unit || ''] + const [, negative, currency, integer, decimal, exponentUnit, exponent, unit] = parsedResult + return [negative || '', currency || '', integer, decimal || '', exponentUnit || '', exponent || '', unit || ''] } - return ['', '', '0', '', '', ''] // [negative, currency, integer, decimal, exponent, unit] + return ['', '', '0', '', '', '', ''] } const parseString = (value: string): Fraction => { try { - const [negative, _currency, integer, decimal, e, _unit] = parseNumPart(value) - const exponent = Number(e || '0') - decimal.length + const [negative, _currency, integer, decimal, exponentUnit, e, _unit] = parseNumPart(value) + const trimedNumerator = (negative + integer.replace(/,/g, '') + decimal).replace(/^0+/, '').replace(/^-0+/, '-') + const exponent = + Number(e || '0') + + Number(exponentUnit ? unitMapping[exponentUnit.toLowerCase().trim()] ?? '0' : '0') - + decimal.length + if (exponent > 0) { - return new Fraction(negative + integer.replace(/,/g, '') + decimal + '0'.repeat(exponent), 1) + return new Fraction(trimedNumerator + '0'.repeat(exponent), 1) } - return new Fraction(negative + integer.replace(/,/g, '') + decimal, 10 ** -exponent) + return new Fraction(trimedNumerator, '1' + '0'.repeat(-exponent)) } catch (e) { return new Fraction(0, 1) } @@ -105,12 +117,12 @@ export const parseFraction = (value: FormatValue): Fraction => { value instanceof Price ) { const valueStr = (() => { - if (typeof value === 'string') return parseString(value).toFixed(18) + if (typeof value === 'string') return parseString(value).toFixed(100) if (typeof value === 'number') return toString(value) if (value instanceof BigNumber) return value.toString() if (value instanceof CurrencyAmount) return value.toFixed(value.currency.decimals) if (value instanceof Price) return value.toFixed(18) - if (value instanceof Percent) return value.divide(100).toFixed(18) + if (value instanceof Percent) return value.divide(100).toFixed(100) return '0' })() return new Fraction(valueStr.replace('.', ''), '1' + '0'.repeat(valueStr.split('.')[1]?.length || 0)) @@ -131,8 +143,8 @@ type FormatOptions = { fractionDigits?: number // usually for percent & currency styles significantDigits?: number // usually for decimal style fallback?: string - allowNegative?: boolean - allowZero?: boolean + allowDisplayNegative?: boolean + allowDisplayZero?: boolean } interface RequiredFraction extends FormatOptions { fractionDigits: number // usually for percent & currency styles @@ -163,8 +175,8 @@ export const formatDisplayNumber = ( significantDigits, fractionDigits, fallback = '--', - allowNegative = false, - allowZero = true, + allowDisplayNegative = false, + allowDisplayZero = true, }: RequiredFraction | RequiredSignificant, ): string => { const currency = style === 'currency' ? '$' : '' @@ -173,20 +185,20 @@ export const formatDisplayNumber = ( if (value === undefined || value === null || Number.isNaN(value)) return fallbackResult const parsedFraction = parseFraction(value) - if (!allowNegative && parsedFraction.lessThan(BIG_INT_ZERO)) return fallbackResult - if (!allowZero && parsedFraction.equalTo(BIG_INT_ZERO)) return fallbackResult + if (!allowDisplayNegative && parsedFraction.lessThan(BIG_INT_ZERO)) return fallbackResult + if (!allowDisplayZero && parsedFraction.equalTo(BIG_INT_ZERO)) return fallbackResult const shownFraction = style === 'percent' ? parsedFraction.multiply(100) : parsedFraction const absShownFraction = shownFraction.lessThan(0) ? shownFraction.multiply(-1) : shownFraction if (absShownFraction.lessThan(BIG_INT_ONE) && !shownFraction.equalTo(BIG_INT_ZERO)) { - const decimal = shownFraction.toSignificant(Math.max(30, significantDigits || 0, fractionDigits || 0)).split('.')[1] + const decimal = shownFraction.toFixed(100).split('.')[1] const negative = shownFraction.lessThan(BIG_INT_ZERO) ? '-' : '' const numberOfLeadingZeros = -Math.floor(log10(absShownFraction) + 1) const slicedDecimal = decimal .replace(/^0+/, '') - .slice(0, fractionDigits) - .slice(0, significantDigits || 6) + .slice(0, fractionDigits ? fractionDigits : 30) + .slice(0, significantDigits ? significantDigits : 30) .replace(/0+$/, '') if (numberOfLeadingZeros > 3) { @@ -213,20 +225,8 @@ export const formatDisplayNumber = ( maximumFractionDigits: fractionDigits, minimumSignificantDigits: significantDigits ? 1 : undefined, maximumSignificantDigits: significantDigits, + roundingPriority: fractionDigits && significantDigits ? 'lessPrecision' : undefined, }) - const result = formatter.format( - Number(parsedFraction.toSignificant(Math.max(30, significantDigits || 0, fractionDigits || 0))), - ) - - // Intl.NumberFormat does not handle maximumFractionDigits well when used along with maximumSignificantDigits - // It might return number with longer fraction digits than maximumFractionDigits - // Hence, we have to do an additional step that manually slice those oversize fraction digits - if (fractionDigits !== undefined) { - const [negative, currency, integer, decimal, _exponent, unit] = parseNumPart(result) - const trimedSlicedDecimal = decimal?.slice(0, fractionDigits).replace(/0+$/, '') - if (trimedSlicedDecimal) return negative + currency + integer + '.' + trimedSlicedDecimal + unit - return negative + currency + integer + unit - } - return result + return formatter.format(Number(parsedFraction.toFixed(100))) } diff --git a/yarn.lock b/yarn.lock index 8f867837fd..1dec1b3ade 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4784,11 +4784,6 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240" integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ== -"@types/node@^13.13.52": - version "13.13.52" - resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.52.tgz#03c13be70b9031baaed79481c0c0cfb0045e53f7" - integrity sha512-s3nugnZumCC//n4moGGe6tkNMyYEdaDBitVjwPxXmR5lnMG5dHePinH2EdxkG3Rh1ghFHHixAG4NJhpJW1rthQ== - "@types/node@^14.14.31": version "14.18.42" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.42.tgz#fa39b2dc8e0eba61bdf51c66502f84e23b66e114" @@ -4799,6 +4794,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.48.tgz#3bc872236cdb31cb51024d8875d655e25db489a4" integrity sha512-mlaecDKQ7rIZrYD7iiKNdzFb6e/qD5I9U1rAhq+Fd+DWvYVs+G2kv74UFHmSOlg5+i/vF3XxuR522V4u8BqO+Q== +"@types/node@^20.8.8": + version "20.8.8" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.8.tgz#adee050b422061ad5255fc38ff71b2bb96ea2a0e" + integrity sha512-YRsdVxq6OaLfmR9Hy816IMp33xOBjfyOgUd77ehqg96CFywxAPbDbXvAsuN2KVg2HOT8Eh6uAfU+l4WffwPVrQ== + dependencies: + undici-types "~5.25.1" + "@types/normalize-package-data@^2.4.0": version "2.4.1" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" @@ -17088,6 +17090,11 @@ underscore@^1.13.6: resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.6.tgz#04786a1f589dc6c09f761fc5f45b89e935136441" integrity sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A== +undici-types@~5.25.1: + version "5.25.3" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.25.3.tgz#e044115914c85f0bcbb229f346ab739f064998c3" + integrity sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA== + unique-string@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-3.0.0.tgz#84a1c377aff5fd7a8bc6b55d8244b2bd90d75b9a"