diff --git a/example/custom-bar-chart/custom-chart.ts b/example/custom-bar-chart/custom-chart.ts index 7848daa..80d9fc4 100644 --- a/example/custom-bar-chart/custom-chart.ts +++ b/example/custom-bar-chart/custom-chart.ts @@ -9,6 +9,7 @@ */ import { + AppConfig, ChartColumn, ChartConfig, ChartModel, @@ -17,9 +18,9 @@ import { ColumnType, CustomChartContext, DataPointsArray, - dateFormatter, getCfForColumn, getChartContext, + getCustomCalendarGuidFromColumn, isDateColumn, isDateNumColumn, PointVal, @@ -29,16 +30,30 @@ import { VisualProps, } from '@thoughtspot/ts-chart-sdk'; import { ChartConfigEditorDefinition } from '@thoughtspot/ts-chart-sdk/src'; +import { + generateMapOptions, + getDataFormatter, +} from '@thoughtspot/ts-chart-sdk/src/utils/formatting-util'; import Chart from 'chart.js/auto'; import ChartDataLabels from 'chartjs-plugin-datalabels'; import _ from 'lodash'; -import { availableColor, getBackgroundColor, getPlotLinesAndBandsFromConditionalFormatting, visualPropKeyMap } from './custom-chart.utils'; -import { createPlotbandPlugin, createPlotlinePlugin } from './custom-chart-plugins'; +import { + availableColor, + getBackgroundColor, + getPlotLinesAndBandsFromConditionalFormatting, + visualPropKeyMap, +} from './custom-chart.utils'; +import { + createPlotbandPlugin, + createPlotlinePlugin, +} from './custom-chart-plugins'; Chart.register(ChartDataLabels); let globalChartReference: Chart; +let appConfigGlobal: AppConfig; + const exampleClientState = { id: 'chart-id', name: 'custom-bar-chart', @@ -46,14 +61,21 @@ const exampleClientState = { }; function getDataForColumn(column: ChartColumn, dataArr: DataPointsArray) { + const formatter = getDataFormatter(column, { isMillisIncluded: false }); const idx = _.findIndex(dataArr.columns, (colId) => column.id === colId); - return _.map(dataArr.dataValue, (row) => { + const dataForCol = _.map(dataArr.dataValue, (row) => { const colValue = row[idx]; - if (isDateColumn(column) || isDateNumColumn(column)) { - return dateFormatter(colValue, column); - } return colValue; }); + const options = generateMapOptions(appConfigGlobal, column, dataForCol); + const formattedValuesForData = _.map(dataArr.dataValue, (row) => { + const colValue = row[idx]; + if (getCustomCalendarGuidFromColumn(column)) + return formatter(colValue.v.s, options); + return formatter(colValue, options); + }); + + return formattedValuesForData; } function getColumnDataModel( @@ -182,6 +204,7 @@ function insertCustomFont(customFontFaces) { function render(ctx: CustomChartContext) { const chartModel = ctx.getChartModel(); const appConfig = ctx.getAppConfig(); + appConfigGlobal = appConfig; ctx.emitEvent(ChartToTSEvent.UpdateVisualProps, { visualProps: JSON.parse( @@ -487,6 +510,16 @@ const renderChart = async (ctx: CustomChartContext): Promise => { }, allowedConfigurations: { allowColumnConditionalFormatting: true, + allowMeasureNamesAndValues: true, + }, + chartConfigParameters: { + measureNameValueColumns: { + enableMeasureNameColumn: true, + enableMeasureValueColumn: true, + measureNameColumnAlias: 'Name', + measureValueColumnAlias: 'Value', + }, + batchSizeLimit: 20000, }, }); diff --git a/package-lock.json b/package-lock.json index 08c03e7..b2bb8ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "0.0.2-alpha.18", "license": "ThoughtSpot Development Tools End User License Agreement", "dependencies": { + "cldr-data": "^36.0.2", + "globalize": "^1.7.0", "lodash": "^4.17.21", "luxon": "^3.4.4", "promise-postmessage": "^3.5.0", @@ -18,6 +20,7 @@ "devDependencies": { "@testing-library/react": "12.1.5", "@testing-library/react-hooks": "^7.0.2", + "@types/globalize": "^1.5.5", "@types/jest": "^27.0.3", "@types/lodash": "4.14.175", "@types/luxon": "^3.4.2", @@ -734,6 +737,95 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "dev": true, @@ -1141,6 +1233,15 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.18.0", "cpu": [ @@ -1281,6 +1382,12 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/cldrjs": { + "version": "0.4.28", + "resolved": "https://registry.npmjs.org/@types/cldrjs/-/cldrjs-0.4.28.tgz", + "integrity": "sha512-3sU6qBTMONeM8BvBzKtylN7Q9xXwaJVc2DvGa9p3HsTvo+rhExRNSu0bapsxf/AVl2x0ZKVY7wKrRNglQ4SQzA==", + "dev": true + }, "node_modules/@types/detect-indent": { "version": "0.1.30", "dev": true, @@ -1299,6 +1406,15 @@ "@types/node": "*" } }, + "node_modules/@types/globalize": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@types/globalize/-/globalize-1.5.5.tgz", + "integrity": "sha512-UINIthRJ3ElD3JP4kTiX+YNkctR6duTCpyzcEkafiGLxoLowvW9WiL2miayN45g9mOVqrBy2a1tgFQuCD6vdOw==", + "dev": true, + "dependencies": { + "@types/cldrjs": "*" + } + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "dev": true, @@ -1637,6 +1753,11 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, "node_modules/acorn": { "version": "7.4.1", "dev": true, @@ -1726,7 +1847,6 @@ }, "node_modules/ansi-regex": { "version": "5.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -1739,7 +1859,6 @@ }, "node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -2039,6 +2158,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axios": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", + "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", + "dependencies": { + "follow-redirects": "^1.14.8" + } + }, "node_modules/babel-eslint": { "version": "10.1.0", "dev": true, @@ -2155,7 +2282,6 @@ }, "node_modules/balanced-match": { "version": "1.0.2", - "dev": true, "license": "MIT" }, "node_modules/brace-expansion": { @@ -2233,6 +2359,14 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "engines": { + "node": "*" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "dev": true, @@ -2377,6 +2511,98 @@ "dev": true, "license": "MIT" }, + "node_modules/cldr-data": { + "version": "36.0.2", + "resolved": "https://registry.npmjs.org/cldr-data/-/cldr-data-36.0.2.tgz", + "integrity": "sha512-JaZY8l0LuJNqc8USVrFPH0KWkYCRxYUB34ALr3TZFHOXDS3R4x5M49d/NTp8Cbucjh2l1Xk2cl9yif1RdWBGBw==", + "hasInstallScript": true, + "dependencies": { + "cldr-data-downloader": "1.0.0-1", + "glob": "10.3.12" + } + }, + "node_modules/cldr-data-downloader": { + "version": "1.0.0-1", + "resolved": "https://registry.npmjs.org/cldr-data-downloader/-/cldr-data-downloader-1.0.0-1.tgz", + "integrity": "sha512-jskJncLkJlkBCdqdgzLSV9sOOLyEdeVOtwJOwVwRyliVJ+4822KZWvfaD620c9Lk7el3auwFDg92FXYjGA5BhQ==", + "dependencies": { + "axios": "^0.26.0", + "mkdirp": "0.5.5", + "nopt": "3.0.x", + "q": "1.0.1", + "yauzl": "^2.10.0" + }, + "bin": { + "cldr-data-downloader": "bin/download.sh" + } + }, + "node_modules/cldr-data-downloader/node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/cldr-data-downloader/node_modules/mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/cldr-data/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cldr-data/node_modules/glob": { + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.6", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cldr-data/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cldrjs": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/cldrjs/-/cldrjs-0.5.5.tgz", + "integrity": "sha512-KDwzwbmLIPfCgd8JERVDpQKrUUM1U4KpFJJg2IROv89rF172lLufoJnqJ/Wea6fXL5bO6WjuLMzY8V52UWPvkA==" + }, "node_modules/cli-cursor": { "version": "2.1.0", "dev": true, @@ -2419,7 +2645,6 @@ }, "node_modules/color-convert": { "version": "2.0.1", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -2430,7 +2655,6 @@ }, "node_modules/color-name": { "version": "1.1.4", - "dev": true, "license": "MIT" }, "node_modules/combined-stream": { @@ -2474,7 +2698,6 @@ }, "node_modules/cross-spawn": { "version": "7.0.3", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -2830,6 +3053,11 @@ "dev": true, "license": "MIT" }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, "node_modules/electron-to-chromium": { "version": "1.4.816", "dev": true, @@ -2848,7 +3076,6 @@ }, "node_modules/emoji-regex": { "version": "8.0.0", - "dev": true, "license": "MIT" }, "node_modules/error-ex": { @@ -4342,6 +4569,14 @@ "bser": "2.1.1" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/figures": { "version": "2.0.0", "dev": true, @@ -4416,6 +4651,25 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "dev": true, @@ -4424,6 +4678,32 @@ "is-callable": "^1.1.3" } }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.0", "dev": true, @@ -4594,6 +4874,14 @@ "node": ">= 6" } }, + "node_modules/globalize": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/globalize/-/globalize-1.7.0.tgz", + "integrity": "sha512-faR46vTIbFCeAemyuc9E6/d7Wrx9k2ae2L60UhakztFg6VuE42gENVJNuPFtt7Sdjrk9m2w8+py7Jj+JTNy59w==", + "dependencies": { + "cldrjs": "^0.5.4" + } + }, "node_modules/globals": { "version": "13.24.0", "dev": true, @@ -5221,7 +5509,6 @@ }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5467,7 +5754,6 @@ }, "node_modules/isexe": { "version": "2.0.0", - "dev": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -5551,6 +5837,23 @@ "set-function-name": "^2.0.1" } }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jest": { "version": "27.5.1", "dev": true, @@ -6878,6 +7181,14 @@ "node": ">= 6" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mkdirp": { "version": "0.5.6", "dev": true, @@ -6949,6 +7260,17 @@ "dev": true, "license": "MIT" }, + "node_modules/nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, "node_modules/normalize-package-data": { "version": "3.0.3", "dev": true, @@ -7407,7 +7729,6 @@ }, "node_modules/path-key": { "version": "3.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7418,6 +7739,26 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, "node_modules/path-type": { "version": "4.0.0", "dev": true, @@ -7426,6 +7767,11 @@ "node": ">=8" } }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" + }, "node_modules/picocolors": { "version": "1.0.1", "dev": true, @@ -7675,6 +8021,16 @@ "node": ">=6" } }, + "node_modules/q": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.0.1.tgz", + "integrity": "sha512-18MnBaCeBX9sLRUdtxz/6onlb7wLzFxCylklyO8n27y5JxJYaGLPu4ccyc5zih58SpEzY8QmfwaWqguqXU6Y+A==", + "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, "node_modules/querystringify": { "version": "2.2.0", "dev": true, @@ -8237,7 +8593,6 @@ }, "node_modules/shebang-command": { "version": "2.0.0", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -8248,7 +8603,6 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8410,7 +8764,6 @@ }, "node_modules/string-width": { "version": "4.2.3", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -8421,6 +8774,20 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.11", "dev": true, @@ -8511,7 +8878,6 @@ }, "node_modules/strip-ansi": { "version": "6.0.1", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -8520,6 +8886,18 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "4.0.0", "dev": true, @@ -9240,7 +9618,6 @@ }, "node_modules/which": { "version": "2.0.2", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -9351,6 +9728,23 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "dev": true, @@ -9449,6 +9843,15 @@ "node": ">=10" } }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "dev": true, diff --git a/package.json b/package.json index c5315c4..31959bf 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "devDependencies": { "@testing-library/react": "12.1.5", "@testing-library/react-hooks": "^7.0.2", + "@types/globalize": "^1.5.5", "@types/jest": "^27.0.3", "@types/lodash": "4.14.175", "@types/luxon": "^3.4.2", @@ -74,6 +75,8 @@ "vite": "4.2.1" }, "dependencies": { + "cldr-data": "^36.0.2", + "globalize": "^1.7.0", "lodash": "^4.17.21", "luxon": "^3.4.4", "promise-postmessage": "^3.5.0", diff --git a/src/index.ts b/src/index.ts index 9361f08..7a089ca 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,3 +10,5 @@ export * from './types/conditional-formatting.types'; export * from './types/number-formatting.types'; export * from './utils/date-formatting'; export * from './utils/conditional-formatting/conditional-formatting'; +export * from './utils/number-formatting/number-formatting'; +export * from './utils/globalize-setup'; diff --git a/src/types/answer-column.types.ts b/src/types/answer-column.types.ts index a322066..5972566 100644 --- a/src/types/answer-column.types.ts +++ b/src/types/answer-column.types.ts @@ -82,9 +82,9 @@ export enum FormatType { * @version SDK: 0.1 | ThoughtSpot: */ export enum CurrencyFormatType { - USER_LOCALE, - COLUMN, - ISO_CODE, + USER_LOCALE = 'USER_LOCALE' , + COLUMN = 'COLUMN', + ISO_CODE= 'ISO_CODE', } /** diff --git a/src/utils/globalize-setup.ts b/src/utils/globalize-setup.ts new file mode 100644 index 0000000..ad56fc3 --- /dev/null +++ b/src/utils/globalize-setup.ts @@ -0,0 +1,213 @@ +/** + * @file: Initialize Globalize + * + * @author Yashvardhan Nehra + * + * Copyright: ThoughtSpot Inc. 2024 + */ + +/* eslint-disable import/no-extraneous-dependencies */ +import enCaGregorian from 'cldr-data/main/en/ca-gregorian.json'; +import enCurrencies from 'cldr-data/main/en/currencies.json'; +import enNumbers from 'cldr-data/main/en/numbers.json'; +import currencyData from 'cldr-data/supplemental/currencyData.json'; +import supplemental from 'cldr-data/supplemental/likelySubtags.json'; +import enpluralJson from 'cldr-data/supplemental/plurals.json'; +import Globalize from 'globalize'; +import _ from 'lodash'; + +let currentLocale: string; +let currentCurrencyFormat: any; +let supplementalCurrencyDataJson: any; + +/** + * Extracts the country code from a locale string. + * + * @param locale - The locale string (e.g., 'en-US', 'en_US'). + * @returns The country code (e.g., 'US') or the input locale if no delimiter is found. + */ +export const getCountryCode = (locale: string): string => { + let parts = locale.split('_'); // // Split by '_' + if (parts.length === 2) { + return parts[1]; + } + parts = locale.split('-'); // // Split by '-' + if (parts.length === 2) { + return parts[1]; + } + return locale; +}; + +/** + * Retrieves the default currency code for the current locale. + * + * @returns The default currency code (e.g., 'USD') or GBP if not found. + */ +export const getDefaultCurrencyCode = (): string => { + if (currentCurrencyFormat) { + return currentCurrencyFormat; + } + + const countryCode = getCountryCode(currentLocale); + const regionData = + supplementalCurrencyDataJson?.supplemental?.currencyData?.region[ + countryCode.toUpperCase() + ]; + if (!regionData || regionData.length === 0) { + console.warn('No currency data found for country:', countryCode); + return 'GBP'; + } + return Object.keys(regionData[regionData.length - 1])[0]; +}; + +/** + * Sets the current locale for Globalize and updates the global state. + * + * @param locale - The locale string to set (e.g., 'en-US'). + */ +export const setGlobalizeLocale = (locale: string): void => { + Globalize.locale(locale); + currentLocale = locale; +}; + +/** + * Retrieves the current locale set in Globalize. + * + * @returns The current locale string (e.g., 'en-US'). + */ +export const getGlobalizeLocale = () => currentLocale; + +/** + * Updates the current currency format. + * + * @param currencyFormat - The currency format to set. + */ +export const setCurrentCurrencyFormat = (currencyFormat: any): void => { + currentCurrencyFormat = currencyFormat; +}; + +/** + * Retrieves the current currency format. + * + * @returns The current currency format. + */ +export const getCurrentCurrencyFormat = () => currentCurrencyFormat; + +/** + * Loads supplemental currency data for Globalize. + * + * @param data - The supplemental currency data to load. + */ +export const loadCurrencyData = (data: any) => { + supplementalCurrencyDataJson = data; +}; + +/** + * Loads CLDR data into Globalize. + * + * @param data - The CLDR data to load. + */ +export const loadGlobalizeData = (data: any) => { + Globalize.load(data); +}; + +/** + * Initializes Globalize with CLDR data and sets the default locale. + * + * @param locale - The locale to initialize Globalize with (default: 'en-gb'). + */ +export const initializeGlobalize = (locale = 'en-gb') => { + loadGlobalizeData(enNumbers); + loadGlobalizeData(enCaGregorian); + loadGlobalizeData(supplemental); + loadGlobalizeData(currencyData); + loadGlobalizeData(enpluralJson); + loadGlobalizeData(enCurrencies); + + loadCurrencyData(currencyData); + + setGlobalizeLocale(locale); +}; + +/** + * Creates a number formatter with the given options. + * + * @param format - The Globalize number formatter options. + * @returns A formatter function for numbers. + */ +export function globalizeNumberFormatter( + format: Globalize.NumberFormatterOptions, +): (num: number) => string { + return Globalize.numberFormatter(format); +} + +/** + * Creates a currency formatter with the given options. + * + * @param currencyCode - The ISO currency code (e.g., 'USD'). + * @param format - The Globalize currency formatter options. + * @returns A formatter function for currency values. + */ +export function globalizeCurrencyFormatter( + currencyCode: string, + format: Globalize.CurrencyFormatterOptions, +): (num: number) => string { + return Globalize.currencyFormatter(currencyCode, format); +} + +/** + * Formats a number using Globalize, handling errors gracefully. + * + * @param format - Globalize number formatter options. + * @param num - The number to format. + * @returns The formatted number as a string. + */ +export function formatNumberSafely< + FormatOptions extends Globalize.NumberFormatterOptions +>(format: FormatOptions, num: number): string { + try { + const formatter = globalizeNumberFormatter(format); + const formattedNumber = formatter(num); + return formattedNumber; + } catch (e) { + console.error('Error formatting pattern: ', format, num, e); + if (Math.abs(num) < 1e-7) { + return '0'; + } + return String(num); + } +} + +/** + * Sanitizes a number format to ensure compatibility with Globalize. + * + * @param format - The raw format string (e.g., '#.##'). + * @returns A sanitized format string (e.g., '0.##'). + */ +export const sanitizeFormat = (format: string): string => { + // Globalize needs to have a zero before the decimal point + // or at the end of format if no decimal point + let sanitizedFormat = format.replace(/#\./, '0.'); + if (!sanitizedFormat.includes('.')) { + sanitizedFormat = sanitizedFormat.replace(/#(%?)$/, '0$1'); + } + return sanitizedFormat; +}; + +/** + * Validates if a given number format is compatible with Globalize. + * + * @param format - The raw format string. + * @returns True if the format is valid; otherwise, false. + */ +export const validateNumberFormat = (format: string): boolean => { + try { + Globalize.numberFormatter({ + ...({ raw: sanitizeFormat(format) } as any), + })(123); // Test the formatter with a dummy value + } catch (e) { + console.error('Invalid number format:', format, e); + return false; + } + return true; +}; diff --git a/src/utils/number-formatting/formatting-utils.ts b/src/utils/number-formatting/formatting-utils.ts new file mode 100644 index 0000000..19bf1d1 --- /dev/null +++ b/src/utils/number-formatting/formatting-utils.ts @@ -0,0 +1,269 @@ +/** + * @file: Formatting Utils + * + * @author Yashvardhan Nehra + * + * Copyright: ThoughtSpot Inc. 2024 + */ + +import _ from 'lodash'; +import { + ColumnFormat, + CurrencyFormat, + CurrencyFormatType, +} from '../../types/answer-column.types'; +import { Maybe } from '../../types/common.types'; +import { + CategoryType, + CurrencyFormatConfig, + FormatConfig, + NegativeValueFormat, + NumberFormatConfig, + Unit, +} from '../../types/number-formatting.types'; +import { getDefaultCurrencyCode } from '../globalize-setup'; + +interface FormatterConfig { + unitDetails: Unit; + decimalDetails: number; + shouldRemoveTrailingZeros: boolean; +} + +export const PROTO_TO_NEGATIVE_VALUE_FORMAT = { + 1: NegativeValueFormat.PrefixDash, + 2: NegativeValueFormat.SuffixDash, + 3: NegativeValueFormat.BracesNodash, +}; + +/** + * Constants for unit conversions and suffixes. + */ +export const UNITS_TO_DIVIDING_FACTOR: Record = { + [Unit.None]: 1, + [Unit.Thousands]: 1000, + [Unit.Million]: 1000 * 1000, + [Unit.Billion]: 1000 * 1000 * 1000, + [Unit.Trillion]: 1000 * 1000 * 1000 * 1000, + [Unit.Auto]: 1, +}; + +export const PROTO_TO_UNITS = { + 1: Unit.None, + 2: Unit.Thousands, + 3: Unit.Million, + 4: Unit.Billion, + 5: Unit.Trillion, + 6: Unit.Auto, +}; + +/** + * Default strings for placeholders. + */ +const strings = { + NULL_VALUE_PLACEHOLDER_LABEL: '{Null}', + EMPTY_VALUE_PLACEHOLDER_LABEL: '{Empty}', +}; + +export const UNITS_TO_SUFFIX: Record = { + [Unit.None]: '', + [Unit.Thousands]: 'K', + [Unit.Million]: 'M', + [Unit.Billion]: 'B', + [Unit.Trillion]: 'T', + [Unit.Auto]: '', +}; + +const DEFAULT_DECIMAL_PRECISION = 2; + +/** + * Formats negative values according to the specified format. + * + * @param formattedValue - The formatted value to modify. + * @param negativeFormat - The desired negative value format. + * @returns The formatted negative value. + */ +export const formatNegativeValue = ( + formattedValue: string, + negativeFormat?: Maybe, +): string => { + if (typeof negativeFormat === 'number') { + // eslint-disable-next-line no-param-reassign + negativeFormat = PROTO_TO_NEGATIVE_VALUE_FORMAT[negativeFormat]; + } + switch (negativeFormat) { + case NegativeValueFormat.PrefixDash: + return `-${formattedValue}`; + case NegativeValueFormat.SuffixDash: + return `${formattedValue}-`; + case NegativeValueFormat.BracesNodash: + return `(${formattedValue})`; + default: + return `-${formattedValue}`; + } +}; + +/** + * Determines the appropriate unit for auto-scaling values. + * + * @param value - The numeric value to evaluate. + * @returns The appropriate unit (e.g., Thousand, Million, etc.). + */ +export const getAutoUnit = (value: number): Unit => { + if (value >= UNITS_TO_DIVIDING_FACTOR[Unit.Trillion]) { + return Unit.Trillion; + } + if (value >= UNITS_TO_DIVIDING_FACTOR[Unit.Billion]) { + return Unit.Billion; + } + if (value >= UNITS_TO_DIVIDING_FACTOR[Unit.Million]) { + return Unit.Million; + } + if (value >= UNITS_TO_DIVIDING_FACTOR[Unit.Thousands]) { + return Unit.Thousands; + } + return Unit.None; +}; + +/** + * Retrieves the locale name based on the provided currency format. + * + * @param currencyFormat - The currency format configuration. + * @returns The locale name as a string. + */ +export const getLocaleName = (currencyFormat: CurrencyFormat): string => { + let locale = getDefaultCurrencyCode(); + if (currencyFormat.type === CurrencyFormatType.ISO_CODE) { + locale = currencyFormat.isoCode; + } + return locale; +}; + +/** + * Generates a default format configuration based on the column settings. + * + * @param columnFormatConfig - The column-specific formatting settings. + * @returns The default format configuration. + */ +export const defaultFormatConfig = ( + columnFormatConfig: ColumnFormat, +): FormatConfig => { + if (columnFormatConfig?.pattern) { + return { + __typename: 'FormatConfig', + category: CategoryType.Custom, + isCategoryEditable: true, + customFormatConfig: { + __typename: 'CustomFormatConfig', + format: columnFormatConfig?.pattern, + }, + }; + } + if (columnFormatConfig?.currencyFormat) { + const locale = getLocaleName(columnFormatConfig?.currencyFormat); + const currencyFormatConfig: CurrencyFormatConfig = { + __typename: 'CurrencyFormatConfig', + decimals: 0, + locale, + removeTrailingZeroes: false, + toSeparateThousands: true, + unit: Unit.Auto, + }; + const formatConfig: FormatConfig = { + __typename: 'FormatConfig', + category: CategoryType.Currency, + isCategoryEditable: true, + currencyFormatConfig, + }; + return formatConfig; + } + const numberFormatConfig: NumberFormatConfig = { + __typename: 'NumberFormatConfig', + decimals: 0, + negativeValueFormat: NegativeValueFormat.PrefixDash, + removeTrailingZeroes: false, + toSeparateThousands: true, + unit: Unit.Auto, + }; + const formatConfig: FormatConfig = { + __typename: 'FormatConfig', + category: CategoryType.Number, + isCategoryEditable: true, + numberFormatConfig, + percentageFormatConfig: null, + }; + return formatConfig; +}; + +/** + * Maps configuration details to a formatter configuration object. + * + * @param absFloatValue - The absolute value of the number being formatted. + * @param configDetails - The number or currency format configuration. + * @returns A mapped formatter configuration. + */ +export const mapFormatterConfig = ( + absFloatValue: number, + configDetails: NumberFormatConfig | CurrencyFormatConfig, +): FormatterConfig => { + if (typeof configDetails.unit === 'number') { + // eslint-disable-next-line no-param-reassign + configDetails.unit = PROTO_TO_UNITS[configDetails.unit]; + } + const isAutoFormatted = configDetails.unit === Unit.Auto; + let unitDetails = configDetails.unit || Unit.Auto; + let decimalDetails = configDetails.decimals || 0; + + if (isAutoFormatted) { + unitDetails = getAutoUnit(absFloatValue); + if (unitDetails !== Unit.None) { + decimalDetails = DEFAULT_DECIMAL_PRECISION; + } + } + const shouldRemoveTrailingZeros = + configDetails.removeTrailingZeroes || isAutoFormatted; + return { + unitDetails, + decimalDetails, + shouldRemoveTrailingZeros, + }; +}; + +/** + * Handles formatting of special data values (e.g., null, empty, NaN, Infinity). + * + * @param value - The value to check and format. + * @returns A formatted special value or null if not applicable. + */ +export function formatSpecialDataValue(value: any) { + if ( + value === strings.NULL_VALUE_PLACEHOLDER_LABEL || + value === strings.EMPTY_VALUE_PLACEHOLDER_LABEL + ) { + return value; + } + + if (value === null || value === undefined) { + return strings.NULL_VALUE_PLACEHOLDER_LABEL; + } + // {Empty} placeholder is set for empty string or no characters + // other than spaces. + if (value === '') { + return strings.EMPTY_VALUE_PLACEHOLDER_LABEL; + } + if (value instanceof Array) { + if (!value.length || value[0] === null || value[0] === undefined) { + return strings.NULL_VALUE_PLACEHOLDER_LABEL; + } + + switch (value[0]) { + case 'NaN': + return 'NaN'; + case 'Infinity': + return 'Infinity'; + default: + return null; + } + } + + return null; +} diff --git a/src/utils/number-formatting/number-formatting.ts b/src/utils/number-formatting/number-formatting.ts new file mode 100644 index 0000000..e77c0d0 --- /dev/null +++ b/src/utils/number-formatting/number-formatting.ts @@ -0,0 +1,251 @@ +/** + * @file: Number Formatting Utils + * + * @author Yashvardhan Nehra + * + * Copyright: ThoughtSpot Inc. 2024 + */ +import _ from 'lodash'; +import { + ColumnFormat, + CurrencyFormat, + CurrencyFormatType, +} from '../../types/answer-column.types'; +import { + CategoryType, + FormatConfig, + Unit, +} from '../../types/number-formatting.types'; +import { + formatNumberSafely, + getDefaultCurrencyCode, + globalizeCurrencyFormatter, + globalizeNumberFormatter, + sanitizeFormat, + validateNumberFormat, +} from '../globalize-setup'; +import { + defaultFormatConfig, + formatNegativeValue, + formatSpecialDataValue, + getLocaleName, + mapFormatterConfig, + UNITS_TO_DIVIDING_FACTOR, + UNITS_TO_SUFFIX, +} from './formatting-utils'; + +const CURRENCY_CODE_EXTRACTOR_REGEX = /[\d.,]+/g; + +/** + * Formats a number with a custom pattern and combines it with a currency symbol. + * + * @param value - The numeric value to format. + * @param currencyCode - The ISO currency code (e.g., 'USD'). + * @param formatPattern - The custom format pattern (e.g., '#,##0.00'). + * @returns The formatted value with the currency symbol. + */ +const formatCurrencyWithCustomPattern = ( + value: number, + currencyCode: string, + formatPattern: string, +): string => { + // Sanitize the custom format pattern + const sanitizedPattern = sanitizeFormat(formatPattern); + + // Create a custom number formatter + const customFormatter = globalizeNumberFormatter({ + ...({ raw: sanitizedPattern } as any), + }); + + const formattedValue = customFormatter(value); + + const currencyFormatter = globalizeCurrencyFormatter(currencyCode, { + style: 'symbol', + }); + + const currencySymbol = currencyFormatter(0).replace( + CURRENCY_CODE_EXTRACTOR_REGEX, + '', + ); // Extract the currency symbol + + // Combine the custom formatted value with the currency symbol + return `${currencySymbol}${formattedValue}`; +}; + +/** + * Formats a value based on the provided format configuration and column settings. + * + * @param value - The value to format (can be a string or number). + * @param formatConfigProp - The format configuration for the value. + * @param columnFormatConfig - The column-specific formatting settings. + * @returns The formatted value as a string. + */ +export const getFormattedValue = ( + value: string | number, + formatConfigProp: FormatConfig, + columnFormatConfig: ColumnFormat, +): string => { + let formatConfig = _.cloneDeep(formatConfigProp); + + // Use default configuration if none is provided + if (_.isNil(formatConfig)) { + formatConfig = defaultFormatConfig(columnFormatConfig); + } + // Normalize category to a proper type + if (typeof formatConfig.category === 'number') { + formatConfig.category = CategoryType.Number; + } + + // Handle special data values (e.g., NaN, Infinity) + const specialVal = formatSpecialDataValue(value); + if (specialVal) { + return specialVal; + } + + // Convert value to a float + const floatValue = parseFloat(value.toString()); + const absFloatValue = Math.abs(floatValue); + + if (formatConfig.category === CategoryType.Number) { + const configDetails = formatConfig.numberFormatConfig || {}; + const formatterConfigMap = mapFormatterConfig( + absFloatValue, + configDetails, + ); + const compactValue = + absFloatValue / + UNITS_TO_DIVIDING_FACTOR[formatterConfigMap.unitDetails as Unit]; + const suffix = UNITS_TO_SUFFIX[formatterConfigMap.unitDetails]; + const formattedValue = formatNumberSafely( + { + style: 'decimal', + maximumFractionDigits: formatterConfigMap.decimalDetails, + minimumFractionDigits: formatterConfigMap.shouldRemoveTrailingZeros + ? 0 + : formatterConfigMap.decimalDetails, + useGrouping: configDetails.toSeparateThousands || false, + }, + compactValue, + ); + const absFormattedValue = `${formattedValue}${suffix}`; + + if (absFloatValue !== floatValue) { + return formatNegativeValue( + absFormattedValue, + configDetails.negativeValueFormat, + ); + } + return absFormattedValue; + } + + if (formatConfig.category === CategoryType.Percentage) { + const configDetails = formatConfig.percentageFormatConfig; + const decimalDetails = configDetails?.decimals || 0; + return formatNumberSafely( + { + style: 'percent', + maximumFractionDigits: decimalDetails, + minimumFractionDigits: configDetails?.removeTrailingZeroes + ? 0 + : decimalDetails, + }, + floatValue, + ); + } + + if (formatConfig.category === CategoryType.Currency) { + const configDetails = formatConfig.currencyFormatConfig || {}; + const formatterConfigMap = mapFormatterConfig( + absFloatValue, + configDetails, + ); + const compactValue = + floatValue / + UNITS_TO_DIVIDING_FACTOR[formatterConfigMap.unitDetails]; + + let locale = configDetails.locale || getDefaultCurrencyCode(); + if (locale === CurrencyFormatType.USER_LOCALE) { + locale = getLocaleName({ + type: locale, + } as CurrencyFormat); + } + try { + const formatter = globalizeCurrencyFormatter(locale, { + style: 'symbol', + maximumFractionDigits: formatterConfigMap.decimalDetails, + minimumFractionDigits: formatterConfigMap.shouldRemoveTrailingZeros + ? 0 + : formatterConfigMap.decimalDetails, + useGrouping: configDetails.toSeparateThousands || false, + }); + + const formattedValue = formatter(compactValue); + const currencyCode = formattedValue.replace( + CURRENCY_CODE_EXTRACTOR_REGEX, + '', + ); + /** + * When we format the value in Millions, Billions etc, + * we have to handle the scenario of the unit suffix, currencycode and the formatted + * value We test the presence of numeric character with the help of regex and then + * we append the apt suffix and currencyCode to the formatted value + */ + const suffix = !_.isNil(floatValue) + ? UNITS_TO_SUFFIX[formatterConfigMap.unitDetails] + : ''; + if (/[0-9]$/.test(formattedValue.charAt(0))) { + return `${formattedValue.replace( + currencyCode, + '', + )}${suffix}${currencyCode}`; + } + return `${formattedValue}${suffix}`; + } catch (e) { + console.error( + 'Corrupted format config passed, formatting using default config', + formatConfig, + e, + ); + } + } + + if (formatConfig.category === CategoryType.Custom) { + const formatPattern = formatConfig.customFormatConfig?.format; + const currencyCode = columnFormatConfig.currencyFormat?.isoCode; + if ( + !_.isNil(formatPattern) && + validateNumberFormat(sanitizeFormat(formatPattern)) + ) { + const sanitizedPattern = sanitizeFormat(formatPattern); + if (!_.isNil(currencyCode)) { + /** + * Globalize does not consider format pattern while formatting the currency, + * only the currency specific rules are considered, so need to combine. That's + * what we do in this formatCurrency method + */ + const formattedValueWithLocaleAndPattern = formatCurrencyWithCustomPattern( + floatValue, + currencyCode, + formatPattern, + ); + return `${formattedValueWithLocaleAndPattern}`; + } + return formatNumberSafely( + { + style: 'decimal', + raw: sanitizedPattern, + }, + floatValue as any, + ); + } + console.error('Invalid custom format config passed:', formatConfig); + } + + // Default fallback: Decimal formatting + return formatNumberSafely( + { + style: 'decimal', + }, + floatValue as any, + ); +};