@thoughtspot/ts-chart-sdk 0.0.2-alpha.8 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +126 -50
- package/dist/ts-chart-sdk.d.ts +337 -31
- package/lib/index.d.ts +3 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +3 -0
- package/lib/index.js.map +1 -1
- package/lib/main/custom-chart-context.d.ts +15 -3
- package/lib/main/custom-chart-context.d.ts.map +1 -1
- package/lib/main/custom-chart-context.js +45 -16
- package/lib/main/custom-chart-context.js.map +1 -1
- package/lib/main/custom-chart-context.spec.js +302 -4
- package/lib/main/custom-chart-context.spec.js.map +1 -1
- package/lib/main/logger.d.ts +13 -0
- package/lib/main/logger.d.ts.map +1 -0
- package/lib/main/logger.js +64 -0
- package/lib/main/logger.js.map +1 -0
- package/lib/main/logger.spec.d.ts +2 -0
- package/lib/main/logger.spec.d.ts.map +1 -0
- package/lib/main/logger.spec.js +88 -0
- package/lib/main/logger.spec.js.map +1 -0
- package/lib/main/util.d.ts +2 -0
- package/lib/main/util.d.ts.map +1 -1
- package/lib/main/util.js +11 -0
- package/lib/main/util.js.map +1 -1
- package/lib/main/util.spec.d.ts +2 -0
- package/lib/main/util.spec.d.ts.map +1 -0
- package/lib/main/util.spec.js +73 -0
- package/lib/main/util.spec.js.map +1 -0
- package/lib/react/use-custom-chart-context.d.ts.map +1 -1
- package/lib/react/use-custom-chart-context.js +3 -1
- package/lib/react/use-custom-chart-context.js.map +1 -1
- package/lib/react/use-custom-chart-context.spec.js +0 -1
- package/lib/react/use-custom-chart-context.spec.js.map +1 -1
- package/lib/react/use-custom-chart-context.util.d.ts.map +1 -1
- package/lib/react/use-custom-chart-context.util.js +5 -3
- package/lib/react/use-custom-chart-context.util.js.map +1 -1
- package/lib/types/answer-column.types.d.ts +15 -5
- package/lib/types/answer-column.types.d.ts.map +1 -1
- package/lib/types/answer-column.types.js +10 -3
- package/lib/types/answer-column.types.js.map +1 -1
- package/lib/types/chart-to-ts-event.types.d.ts +2 -0
- package/lib/types/chart-to-ts-event.types.d.ts.map +1 -1
- package/lib/types/chart-to-ts-event.types.js +1 -0
- package/lib/types/chart-to-ts-event.types.js.map +1 -1
- package/lib/types/common.types.d.ts +65 -5
- package/lib/types/common.types.d.ts.map +1 -1
- package/lib/types/common.types.js.map +1 -1
- package/lib/types/conditional-formatting.types.d.ts +1 -16
- package/lib/types/conditional-formatting.types.d.ts.map +1 -1
- package/lib/types/conditional-formatting.types.js.map +1 -1
- package/lib/types/configurator.types.d.ts +3 -1
- package/lib/types/configurator.types.d.ts.map +1 -1
- package/lib/types/number-formatting.types.d.ts +55 -0
- package/lib/types/number-formatting.types.d.ts.map +1 -0
- package/lib/types/number-formatting.types.js +23 -0
- package/lib/types/number-formatting.types.js.map +1 -0
- package/lib/types/ts-to-chart-event.types.d.ts +19 -6
- package/lib/types/ts-to-chart-event.types.d.ts.map +1 -1
- package/lib/types/ts-to-chart-event.types.js +1 -0
- package/lib/types/ts-to-chart-event.types.js.map +1 -1
- package/lib/types/visual-prop.types.d.ts +71 -3
- package/lib/types/visual-prop.types.d.ts.map +1 -1
- package/lib/types/visual-prop.types.js +32 -1
- package/lib/types/visual-prop.types.js.map +1 -1
- package/lib/utils/conditional-formatting/conditional-formatting.spec.js +2 -1
- package/lib/utils/conditional-formatting/conditional-formatting.spec.js.map +1 -1
- package/lib/utils/date-formatting.d.ts +85 -1
- package/lib/utils/date-formatting.d.ts.map +1 -1
- package/lib/utils/date-formatting.js +344 -9
- package/lib/utils/date-formatting.js.map +1 -1
- package/lib/utils/date-formatting.spec.js +303 -32
- package/lib/utils/date-formatting.spec.js.map +1 -1
- package/lib/utils/date-utils.d.ts +6 -0
- package/lib/utils/date-utils.d.ts.map +1 -0
- package/lib/utils/date-utils.js +20 -0
- package/lib/utils/date-utils.js.map +1 -0
- package/lib/utils/date-utils.spec.d.ts +2 -0
- package/lib/utils/date-utils.spec.d.ts.map +1 -0
- package/lib/utils/date-utils.spec.js +63 -0
- package/lib/utils/date-utils.spec.js.map +1 -0
- package/lib/utils/formatting-util.d.ts +10 -0
- package/lib/utils/formatting-util.d.ts.map +1 -0
- package/lib/utils/formatting-util.js +78 -0
- package/lib/utils/formatting-util.js.map +1 -0
- package/lib/utils/formatting-util.spec.d.ts +2 -0
- package/lib/utils/formatting-util.spec.d.ts.map +1 -0
- package/lib/utils/formatting-util.spec.js +247 -0
- package/lib/utils/formatting-util.spec.js.map +1 -0
- package/lib/utils/globalize-Initializer/globalize-utils.d.ts +16 -0
- package/lib/utils/globalize-Initializer/globalize-utils.d.ts.map +1 -0
- package/lib/utils/globalize-Initializer/globalize-utils.js +101 -0
- package/lib/utils/globalize-Initializer/globalize-utils.js.map +1 -0
- package/lib/utils/globalize-Initializer/globalize-utils.spec.d.ts +2 -0
- package/lib/utils/globalize-Initializer/globalize-utils.spec.d.ts.map +1 -0
- package/lib/utils/globalize-Initializer/globalize-utils.spec.js +149 -0
- package/lib/utils/globalize-Initializer/globalize-utils.spec.js.map +1 -0
- package/lib/utils/number-formatting/number-formatting-utils.d.ts +20 -0
- package/lib/utils/number-formatting/number-formatting-utils.d.ts.map +1 -0
- package/lib/utils/number-formatting/number-formatting-utils.js +159 -0
- package/lib/utils/number-formatting/number-formatting-utils.js.map +1 -0
- package/lib/utils/number-formatting/number-formatting-utils.spec.d.ts +2 -0
- package/lib/utils/number-formatting/number-formatting-utils.spec.d.ts.map +1 -0
- package/lib/utils/number-formatting/number-formatting-utils.spec.js +221 -0
- package/lib/utils/number-formatting/number-formatting-utils.spec.js.map +1 -0
- package/lib/utils/number-formatting/number-formatting.d.ts +5 -0
- package/lib/utils/number-formatting/number-formatting.d.ts.map +1 -0
- package/lib/utils/number-formatting/number-formatting.js +128 -0
- package/lib/utils/number-formatting/number-formatting.js.map +1 -0
- package/lib/utils/number-formatting/number-formatting.spec.d.ts +2 -0
- package/lib/utils/number-formatting/number-formatting.spec.d.ts.map +1 -0
- package/lib/utils/number-formatting/number-formatting.spec.js +159 -0
- package/lib/utils/number-formatting/number-formatting.spec.js.map +1 -0
- package/package.json +5 -2
- package/src/index.ts +3 -0
- package/src/main/custom-chart-context.spec.ts +356 -6
- package/src/main/custom-chart-context.ts +182 -15
- package/src/main/logger.spec.ts +114 -0
- package/src/main/logger.ts +97 -0
- package/src/main/util.spec.ts +94 -0
- package/src/main/util.ts +20 -0
- package/src/react/use-custom-chart-context.spec.tsx +0 -1
- package/src/react/use-custom-chart-context.tsx +4 -1
- package/src/react/use-custom-chart-context.util.ts +6 -3
- package/src/types/answer-column.types.ts +29 -6
- package/src/types/chart-to-ts-event.types.ts +7 -0
- package/src/types/common.types.ts +103 -8
- package/src/types/conditional-formatting.types.ts +2 -17
- package/src/types/configurator.types.ts +15 -1
- package/src/types/number-formatting.types.ts +80 -0
- package/src/types/ts-to-chart-event.types.ts +54 -5
- package/src/types/visual-prop.types.ts +197 -3
- package/src/utils/conditional-formatting/conditional-formatting.spec.ts +2 -0
- package/src/utils/date-formatting.spec.ts +377 -32
- package/src/utils/date-formatting.ts +587 -14
- package/src/utils/date-utils.spec.ts +93 -0
- package/src/utils/date-utils.ts +69 -0
- package/src/utils/formatting-util.spec.ts +312 -0
- package/src/utils/formatting-util.ts +234 -0
- package/src/utils/globalize-Initializer/globalize-utils.spec.ts +192 -0
- package/src/utils/globalize-Initializer/globalize-utils.ts +216 -0
- package/src/utils/number-formatting/number-formatting-utils.spec.ts +296 -0
- package/src/utils/number-formatting/number-formatting-utils.ts +260 -0
- package/src/utils/number-formatting/number-formatting.spec.ts +243 -0
- package/src/utils/number-formatting/number-formatting.ts +264 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
import { CustomChartContext } from '../main/custom-chart-context';
|
|
3
|
+
import {
|
|
4
|
+
ChartColumn,
|
|
5
|
+
ColumnAggregationType,
|
|
6
|
+
ColumnTimeBucket,
|
|
7
|
+
ColumnType,
|
|
8
|
+
DataType,
|
|
9
|
+
} from '../types/answer-column.types';
|
|
10
|
+
import {
|
|
11
|
+
CustomCalendarDate,
|
|
12
|
+
dateFormatPresets,
|
|
13
|
+
formatDate,
|
|
14
|
+
formatDateNum,
|
|
15
|
+
getCustomCalendarGuidFromColumn,
|
|
16
|
+
getCustomCalendarValueFromEpoch,
|
|
17
|
+
getDisplayString,
|
|
18
|
+
getEffectiveDateNumDataType,
|
|
19
|
+
hasCustomCalendar,
|
|
20
|
+
isDateColumn,
|
|
21
|
+
isDateNumColumn,
|
|
22
|
+
isDateTimeColumn,
|
|
23
|
+
} from './date-formatting';
|
|
24
|
+
import {
|
|
25
|
+
getCustomCalendarGuid,
|
|
26
|
+
getFormatPatternForBucket,
|
|
27
|
+
showDateFinancialYearFormat,
|
|
28
|
+
} from './date-utils';
|
|
29
|
+
|
|
30
|
+
interface FormatOptionsType {
|
|
31
|
+
isMillisIncluded: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const getBucketization = (col: ChartColumn) => col.timeBucket;
|
|
35
|
+
/**
|
|
36
|
+
* Retrieves the format pattern for a given column based on its bucketization or custom format.
|
|
37
|
+
*
|
|
38
|
+
* This function first checks the column's bucketization and attempts to retrieve a format pattern
|
|
39
|
+
* using `getFormatPatternForBucket`. If no format pattern is found for the bucketization, it falls
|
|
40
|
+
* back to the column's custom format pattern (if available).
|
|
41
|
+
*
|
|
42
|
+
* @param col - The column to retrieve the format pattern for, represented as a `ChartColumn`.
|
|
43
|
+
* @returns The format pattern as a string, either derived from the bucketization or the column's custom format.
|
|
44
|
+
*/
|
|
45
|
+
export const getFormatPattern = (col: ChartColumn): string =>
|
|
46
|
+
getFormatPatternForBucket(getBucketization(col)) || col.format?.pattern;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Retrieves a formatter function for a column based on its type (e.g., date, date_number).
|
|
50
|
+
*
|
|
51
|
+
* This function returns a specialized formatter based on the column's type and format pattern.
|
|
52
|
+
* It first checks if the column is a date column and applies the appropriate date format,
|
|
53
|
+
* considering options like whether the fiscal year or milliseconds are included. If the column is
|
|
54
|
+
* a date number column, it applies date number formatting. For other types of columns, it returns
|
|
55
|
+
* the raw data value without formatting.
|
|
56
|
+
*
|
|
57
|
+
* @param col - The column for which the formatter is to be created, represented as a `ChartColumn`.
|
|
58
|
+
* @param options - The formatting options, including locale, fiscal settings, and custom calendar information.
|
|
59
|
+
*
|
|
60
|
+
* @returns A function that formats a data value based on the column type and format pattern.
|
|
61
|
+
* This returned function takes in the data value and optional additional options.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
*
|
|
65
|
+
* const dateFormatter = getBaseTypeFormatterInstance(column, options);
|
|
66
|
+
* const formattedValue = dateFormatter(dataValue, options);
|
|
67
|
+
* For more info in column
|
|
68
|
+
* @link ChartColumn
|
|
69
|
+
* To see how options are feched see src/example/custom-bar-chart
|
|
70
|
+
*/
|
|
71
|
+
export function getBaseTypeFormatterInstance(
|
|
72
|
+
col: ChartColumn,
|
|
73
|
+
options: FormatOptionsType,
|
|
74
|
+
): any {
|
|
75
|
+
let formatPattern = getFormatPattern(col);
|
|
76
|
+
// TODO: add numberic formatter if the col is numeric.
|
|
77
|
+
if (isDateColumn(col)) {
|
|
78
|
+
const showFinancialFormat = showDateFinancialYearFormat(col);
|
|
79
|
+
const isDateTime = isDateTimeColumn(col);
|
|
80
|
+
|
|
81
|
+
if (!formatPattern) {
|
|
82
|
+
if (isDateTime) {
|
|
83
|
+
if (options.isMillisIncluded) {
|
|
84
|
+
formatPattern =
|
|
85
|
+
dateFormatPresets.DATETIME_SHORT_WITH_MILLIS;
|
|
86
|
+
} else {
|
|
87
|
+
formatPattern =
|
|
88
|
+
dateFormatPresets.DATETIME_SHORT_WITH_SECONDS;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return (dataValue: any, options?: any) => {
|
|
93
|
+
const customCalendarValueFromEpoch: CustomCalendarDate =
|
|
94
|
+
getCustomCalendarValueFromEpoch(
|
|
95
|
+
col,
|
|
96
|
+
dataValue,
|
|
97
|
+
options?.displayToCustomCalendarValueMap,
|
|
98
|
+
);
|
|
99
|
+
const customCalendarDisplayStr = getDisplayString(
|
|
100
|
+
customCalendarValueFromEpoch,
|
|
101
|
+
);
|
|
102
|
+
if (customCalendarDisplayStr) {
|
|
103
|
+
return customCalendarDisplayStr;
|
|
104
|
+
}
|
|
105
|
+
const customCalendarOverridesFiscalOffset =
|
|
106
|
+
hasCustomCalendar(col) &&
|
|
107
|
+
getCustomCalendarGuidFromColumn(col) !==
|
|
108
|
+
getCustomCalendarGuid(
|
|
109
|
+
'fiscal',
|
|
110
|
+
options.defaultDataSourceId,
|
|
111
|
+
options.tsDefinedCustomCalenders,
|
|
112
|
+
);
|
|
113
|
+
const optionsWithFiscalOffset = {
|
|
114
|
+
...options,
|
|
115
|
+
customCalendarOverridesFiscalOffset:
|
|
116
|
+
!!customCalendarOverridesFiscalOffset,
|
|
117
|
+
};
|
|
118
|
+
return formatDate(
|
|
119
|
+
dataValue,
|
|
120
|
+
formatPattern,
|
|
121
|
+
!showFinancialFormat,
|
|
122
|
+
optionsWithFiscalOffset,
|
|
123
|
+
);
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (isDateNumColumn(col)) {
|
|
127
|
+
return (dataValue: any, options?: any) => {
|
|
128
|
+
const customCalendarValueFromEpoch: CustomCalendarDate =
|
|
129
|
+
getCustomCalendarValueFromEpoch(
|
|
130
|
+
col,
|
|
131
|
+
dataValue,
|
|
132
|
+
options?.displayToCustomCalendarValueMap,
|
|
133
|
+
);
|
|
134
|
+
const customCalendarDisplayStr = getDisplayString(
|
|
135
|
+
customCalendarValueFromEpoch,
|
|
136
|
+
);
|
|
137
|
+
if (customCalendarValueFromEpoch && customCalendarDisplayStr) {
|
|
138
|
+
return customCalendarDisplayStr;
|
|
139
|
+
}
|
|
140
|
+
return formatDateNum(
|
|
141
|
+
getEffectiveDateNumDataType(col),
|
|
142
|
+
dataValue,
|
|
143
|
+
formatPattern,
|
|
144
|
+
options,
|
|
145
|
+
);
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return (dataValue: any) => {
|
|
149
|
+
return dataValue;
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Retrieves the appropriate data formatter function for a specified column,
|
|
154
|
+
* based on its type, format options, and an optional aggregation type override.
|
|
155
|
+
*
|
|
156
|
+
*
|
|
157
|
+
* @param col - The column for which the formatter is to be created, represented as a `ChartColumn`.
|
|
158
|
+
* @param options - The formatting options, including settings such as locale, custom calendars, and date constants.
|
|
159
|
+
* @param aggrTypeOverride - Optional override for the column's aggregation type (e.g., `SUM`, `AVERAGE`),
|
|
160
|
+
* which could affect formatting behavior in future implementations.
|
|
161
|
+
*
|
|
162
|
+
* @returns A function to format data values based on the column type, format pattern, and options.
|
|
163
|
+
* This returned function can be used to format individual data values within the column.
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* const formatter = getDataFormatter(column, formatOptions, 'SUM');
|
|
167
|
+
* const formattedValue = formatter(dataValue);
|
|
168
|
+
*/
|
|
169
|
+
export const getDataFormatter = (
|
|
170
|
+
col: ChartColumn,
|
|
171
|
+
options: FormatOptionsType,
|
|
172
|
+
aggrTypeOverride?: ColumnAggregationType,
|
|
173
|
+
) => {
|
|
174
|
+
// TODO: number formatter for column based on column type based on
|
|
175
|
+
// aggregation type
|
|
176
|
+
|
|
177
|
+
return getBaseTypeFormatterInstance(col, options);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Generates a configuration object with formatting and locale-based options for date handling,
|
|
182
|
+
* including custom calendar mapping for a specific column.
|
|
183
|
+
*
|
|
184
|
+
* This function builds an options object that includes locale settings, quarter start month,
|
|
185
|
+
* date formats, and a custom calendar map, which is populated when the column contains a
|
|
186
|
+
* custom calendar GUID. The custom calendar map links display values to custom calendar data
|
|
187
|
+
* for date formatting needs.
|
|
188
|
+
*
|
|
189
|
+
* @param appConfig - The application configuration containing locale, date format settings,
|
|
190
|
+
* and custom calendar definitions.
|
|
191
|
+
* @param col - The column to check for a custom calendar GUID, represented as a `ChartColumn`.
|
|
192
|
+
* @param data - The dataset, used to populate the custom calendar map if a custom calendar
|
|
193
|
+
* GUID is associated with the column.
|
|
194
|
+
*
|
|
195
|
+
* @returns An options object with locale and date formatting details, including a custom
|
|
196
|
+
* calendar map if applicable.
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* const mapOptions = generateMapOptions(appConfig, column, data);
|
|
200
|
+
* const locale = mapOptions.locale;
|
|
201
|
+
*/
|
|
202
|
+
|
|
203
|
+
export const generateMapOptions = (
|
|
204
|
+
appConfig: any,
|
|
205
|
+
col: ChartColumn,
|
|
206
|
+
data: any,
|
|
207
|
+
): any => {
|
|
208
|
+
let customCalenderMap = {};
|
|
209
|
+
|
|
210
|
+
if (!_.isEmpty(getCustomCalendarGuidFromColumn(col))) {
|
|
211
|
+
customCalenderMap = data.reduce(
|
|
212
|
+
(customCalenderMapAcc: any, dataValue: any) => {
|
|
213
|
+
return {
|
|
214
|
+
...customCalenderMapAcc,
|
|
215
|
+
[dataValue.v.s]: dataValue,
|
|
216
|
+
};
|
|
217
|
+
},
|
|
218
|
+
{},
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
locale: appConfig?.localeOptions?.locale,
|
|
223
|
+
quarterStartMonth: appConfig?.localeOptions?.quarterStartMonth,
|
|
224
|
+
tsLocaleBasedDateFormats:
|
|
225
|
+
appConfig?.dateFormatsConfig?.tsLocaleBasedDateFormats,
|
|
226
|
+
tsLocaleBasedStringsFormats:
|
|
227
|
+
appConfig?.dateFormatsConfig?.tsLocaleBasedStringsFormats,
|
|
228
|
+
tsDateConstants: appConfig?.dateFormatsConfig?.tsDateConstants,
|
|
229
|
+
tsDefinedCustomCalenders:
|
|
230
|
+
appConfig?.dateFormatsConfig?.tsDefinedCustomCalenders,
|
|
231
|
+
defaultDataSourceId: appConfig?.dateFormatsConfig?.defaultDataSourceId,
|
|
232
|
+
displayToCustomCalendarValueMap: customCalenderMap,
|
|
233
|
+
};
|
|
234
|
+
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import Globalize from 'globalize';
|
|
2
|
+
import {
|
|
3
|
+
formatNumberSafely,
|
|
4
|
+
getCountryCode,
|
|
5
|
+
getCurrentCurrencyFormat,
|
|
6
|
+
getDefaultCurrencyCode,
|
|
7
|
+
getGlobalizeLocale,
|
|
8
|
+
globalizeCurrencyFormatter,
|
|
9
|
+
globalizeNumberFormatter,
|
|
10
|
+
initGlobalize,
|
|
11
|
+
loadCurrencyData,
|
|
12
|
+
loadGlobalizeData,
|
|
13
|
+
sanitizeFormat,
|
|
14
|
+
setCurrentCurrencyFormat,
|
|
15
|
+
setGlobalizeLocale,
|
|
16
|
+
validateNumberFormat,
|
|
17
|
+
} from './globalize-utils';
|
|
18
|
+
|
|
19
|
+
describe('Initialize Globalize', () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
jest.clearAllMocks();
|
|
22
|
+
initGlobalize('en-US');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('getCountryCode', () => {
|
|
26
|
+
it('should extract country code from locale with underscore', () => {
|
|
27
|
+
expect(getCountryCode('en_US')).toBe('US');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should extract country code from locale with hyphen', () => {
|
|
31
|
+
expect(getCountryCode('en-US')).toBe('US');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should return the input locale if no delimiter is found', () => {
|
|
35
|
+
expect(getCountryCode('en')).toBe('en');
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('getDefaultCurrencyCode', () => {
|
|
40
|
+
it('should return the current currency format if set', () => {
|
|
41
|
+
setCurrentCurrencyFormat('EUR');
|
|
42
|
+
expect(getDefaultCurrencyCode()).toBe('EUR');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should return GBP if no currency data is found', () => {
|
|
46
|
+
setCurrentCurrencyFormat(null);
|
|
47
|
+
loadCurrencyData({});
|
|
48
|
+
expect(getDefaultCurrencyCode()).toBe('GBP');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should retrieve the default currency code from supplemental data', () => {
|
|
52
|
+
loadCurrencyData({
|
|
53
|
+
supplemental: {
|
|
54
|
+
currencyData: {
|
|
55
|
+
region: {
|
|
56
|
+
US: [{ USD: {} }],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
setGlobalizeLocale('en-US');
|
|
62
|
+
expect(getDefaultCurrencyCode()).toBe('USD');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('setGlobalizeLocale', () => {
|
|
67
|
+
it('should set the Globalize locale', () => {
|
|
68
|
+
setGlobalizeLocale('fr-FR');
|
|
69
|
+
expect(getGlobalizeLocale()).toBe('fr-FR');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('getGlobalizeLocale', () => {
|
|
74
|
+
it('should return the current locale', () => {
|
|
75
|
+
expect(getGlobalizeLocale()).toBe('en-US');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('setCurrentCurrencyFormat', () => {
|
|
80
|
+
it('should set the current currency format', () => {
|
|
81
|
+
setCurrentCurrencyFormat('JPY');
|
|
82
|
+
expect(getCurrentCurrencyFormat()).toBe('JPY');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('loadCurrencyData', () => {
|
|
87
|
+
it('should load supplemental currency data', () => {
|
|
88
|
+
const data = {
|
|
89
|
+
supplemental: {
|
|
90
|
+
currencyData: {
|
|
91
|
+
region: {
|
|
92
|
+
US: [{ USD: {} }],
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
setCurrentCurrencyFormat(null);
|
|
98
|
+
loadCurrencyData(data);
|
|
99
|
+
expect(getDefaultCurrencyCode()).toBe('USD');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('loadGlobalizeData', () => {
|
|
104
|
+
it('should load CLDR data', () => {
|
|
105
|
+
jest.spyOn(Globalize, 'load').mockImplementationOnce((data) => {
|
|
106
|
+
return data;
|
|
107
|
+
});
|
|
108
|
+
const data = { key: 'value' };
|
|
109
|
+
loadGlobalizeData(data);
|
|
110
|
+
expect(Globalize.load).toHaveBeenCalledWith(data);
|
|
111
|
+
jest.clearAllMocks();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('globalizeNumberFormatter', () => {
|
|
116
|
+
it('should create a number formatter', () => {
|
|
117
|
+
const formatter = globalizeNumberFormatter({
|
|
118
|
+
minimumFractionDigits: 2,
|
|
119
|
+
});
|
|
120
|
+
expect(typeof formatter).toBe('function');
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('globalizeCurrencyFormatter', () => {
|
|
125
|
+
it('should create a currency formatter', () => {
|
|
126
|
+
const formatter = globalizeCurrencyFormatter('USD', {
|
|
127
|
+
style: 'symbol',
|
|
128
|
+
});
|
|
129
|
+
expect(typeof formatter).toBe('function');
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('formatNumberSafely', () => {
|
|
134
|
+
it('should format numbers safely', () => {
|
|
135
|
+
const formattedNumber = formatNumberSafely(
|
|
136
|
+
{ maximumFractionDigits: 2, minimumFractionDigits: 2 },
|
|
137
|
+
123.456,
|
|
138
|
+
);
|
|
139
|
+
expect(formattedNumber).toBe('123.46');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should return 0 for very small numbers', () => {
|
|
143
|
+
jest.spyOn(Globalize, 'numberFormatter').mockImplementationOnce(
|
|
144
|
+
() => {
|
|
145
|
+
throw new Error('Test Error');
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
const formattedNumber = formatNumberSafely({}, 1e-8);
|
|
149
|
+
expect(formattedNumber).toBe('0');
|
|
150
|
+
jest.clearAllMocks();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should return the string representation for errors', () => {
|
|
154
|
+
jest.spyOn(Globalize, 'numberFormatter').mockImplementationOnce(
|
|
155
|
+
() => {
|
|
156
|
+
throw new Error('Test Error');
|
|
157
|
+
},
|
|
158
|
+
);
|
|
159
|
+
const formattedNumber = formatNumberSafely({}, 123);
|
|
160
|
+
expect(formattedNumber).toBe('123');
|
|
161
|
+
jest.clearAllMocks();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('sanitizeFormat', () => {
|
|
166
|
+
it('should add zero before decimal point', () => {
|
|
167
|
+
expect(sanitizeFormat('#.##')).toBe('0.##');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should add zero at the end if no decimal point', () => {
|
|
171
|
+
expect(sanitizeFormat('#%')).toBe('0%');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should return the same format if no changes are needed', () => {
|
|
175
|
+
expect(sanitizeFormat('0.##')).toBe('0.##');
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('validateNumberFormat', () => {
|
|
180
|
+
it('should return true for valid number formats', () => {
|
|
181
|
+
expect(validateNumberFormat('#.##')).toBe(true);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should return false for invalid number formats', () => {
|
|
185
|
+
jest.spyOn(Globalize, 'numberFormatter').mockImplementation(() => {
|
|
186
|
+
throw new Error('Test Error');
|
|
187
|
+
});
|
|
188
|
+
expect(validateNumberFormat('#.#')).toBe(false);
|
|
189
|
+
jest.clearAllMocks();
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file: Initialize Globalize
|
|
3
|
+
*
|
|
4
|
+
* @author Yashvardhan Nehra <yashvardhan.nehra@thoughtspot.com>
|
|
5
|
+
*
|
|
6
|
+
* Copyright: ThoughtSpot Inc. 2024
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/* eslint-disable import/no-extraneous-dependencies */
|
|
10
|
+
import enCaGregorian from 'cldr-data/main/en/ca-gregorian.json';
|
|
11
|
+
import enCurrencies from 'cldr-data/main/en/currencies.json';
|
|
12
|
+
import enNumbers from 'cldr-data/main/en/numbers.json';
|
|
13
|
+
import currencyData from 'cldr-data/supplemental/currencyData.json';
|
|
14
|
+
import supplemental from 'cldr-data/supplemental/likelySubtags.json';
|
|
15
|
+
import enpluralJson from 'cldr-data/supplemental/plurals.json';
|
|
16
|
+
import Globalize from 'globalize';
|
|
17
|
+
import _ from 'lodash';
|
|
18
|
+
import { create } from '../../main/logger';
|
|
19
|
+
|
|
20
|
+
const logger = create('globalize-initializer');
|
|
21
|
+
|
|
22
|
+
let currentLocale: string;
|
|
23
|
+
let currentCurrencyFormat: any;
|
|
24
|
+
let supplementalCurrencyDataJson: any;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Extracts the country code from a locale string.
|
|
28
|
+
*
|
|
29
|
+
* @param locale - The locale string (e.g., 'en-US', 'en_US').
|
|
30
|
+
* @returns The country code (e.g., 'US') or the input locale if no delimiter is found.
|
|
31
|
+
*/
|
|
32
|
+
export const getCountryCode = (locale: string): string => {
|
|
33
|
+
let parts = locale.split('_'); // // Split by '_'
|
|
34
|
+
if (parts.length === 2) {
|
|
35
|
+
return parts[1].toUpperCase();
|
|
36
|
+
}
|
|
37
|
+
parts = locale.split('-'); // // Split by '-'
|
|
38
|
+
if (parts.length === 2) {
|
|
39
|
+
return parts[1].toUpperCase();
|
|
40
|
+
}
|
|
41
|
+
return locale;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Retrieves the default currency code for the current locale.
|
|
46
|
+
*
|
|
47
|
+
* @returns The default currency code (e.g., 'USD') or GBP if not found.
|
|
48
|
+
*/
|
|
49
|
+
export const getDefaultCurrencyCode = (): string => {
|
|
50
|
+
if (currentCurrencyFormat) {
|
|
51
|
+
return currentCurrencyFormat;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const countryCode = getCountryCode(currentLocale);
|
|
55
|
+
const regionData =
|
|
56
|
+
supplementalCurrencyDataJson?.supplemental?.currencyData?.region[
|
|
57
|
+
countryCode.toUpperCase()
|
|
58
|
+
];
|
|
59
|
+
if (!regionData || regionData.length === 0) {
|
|
60
|
+
logger.warn('No currency data found for country:', countryCode);
|
|
61
|
+
return 'GBP';
|
|
62
|
+
}
|
|
63
|
+
return Object.keys(regionData[regionData.length - 1])[0];
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Sets the current locale for Globalize and updates the global state.
|
|
68
|
+
*
|
|
69
|
+
* @param locale - The locale string to set (e.g., 'en-US').
|
|
70
|
+
*/
|
|
71
|
+
export const setGlobalizeLocale = (locale: string): void => {
|
|
72
|
+
Globalize.locale(locale);
|
|
73
|
+
currentLocale = locale;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Retrieves the current locale set in Globalize.
|
|
78
|
+
*
|
|
79
|
+
* @returns The current locale string (e.g., 'en-US').
|
|
80
|
+
*/
|
|
81
|
+
export const getGlobalizeLocale = () => currentLocale;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Updates the current currency format.
|
|
85
|
+
*
|
|
86
|
+
* @param currencyFormat - The currency format to set.
|
|
87
|
+
*/
|
|
88
|
+
export const setCurrentCurrencyFormat = (currencyFormat: any): void => {
|
|
89
|
+
currentCurrencyFormat = currencyFormat;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Retrieves the current currency format.
|
|
94
|
+
*
|
|
95
|
+
* @returns The current currency format.
|
|
96
|
+
*/
|
|
97
|
+
export const getCurrentCurrencyFormat = () => currentCurrencyFormat;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Loads supplemental currency data for Globalize.
|
|
101
|
+
*
|
|
102
|
+
* @param data - The supplemental currency data to load.
|
|
103
|
+
*/
|
|
104
|
+
export const loadCurrencyData = (data: any) => {
|
|
105
|
+
supplementalCurrencyDataJson = data;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Loads CLDR data into Globalize.
|
|
110
|
+
*
|
|
111
|
+
* @param data - The CLDR data to load.
|
|
112
|
+
*/
|
|
113
|
+
export const loadGlobalizeData = (data: any) => {
|
|
114
|
+
Globalize.load(data);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Initializes Globalize with CLDR data and sets the default locale.
|
|
119
|
+
*
|
|
120
|
+
* @param locale - The locale to initialize Globalize with (default: 'en-GB').
|
|
121
|
+
*/
|
|
122
|
+
export const initGlobalize = (locale = 'en-GB') => {
|
|
123
|
+
loadGlobalizeData(enNumbers);
|
|
124
|
+
loadGlobalizeData(enCaGregorian);
|
|
125
|
+
loadGlobalizeData(supplemental);
|
|
126
|
+
loadGlobalizeData(currencyData);
|
|
127
|
+
loadGlobalizeData(enpluralJson);
|
|
128
|
+
loadGlobalizeData(enCurrencies);
|
|
129
|
+
|
|
130
|
+
loadCurrencyData(currencyData);
|
|
131
|
+
|
|
132
|
+
setGlobalizeLocale(locale);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Creates a number formatter with the given options.
|
|
137
|
+
*
|
|
138
|
+
* @param format - The Globalize number formatter options.
|
|
139
|
+
* @returns A formatter function for numbers.
|
|
140
|
+
*/
|
|
141
|
+
export function globalizeNumberFormatter(
|
|
142
|
+
format: Globalize.NumberFormatterOptions,
|
|
143
|
+
): (num: number) => string {
|
|
144
|
+
return Globalize.numberFormatter(format);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Creates a currency formatter with the given options.
|
|
149
|
+
*
|
|
150
|
+
* @param currencyCode - The ISO currency code (e.g., 'USD').
|
|
151
|
+
* @param format - The Globalize currency formatter options.
|
|
152
|
+
* @returns A formatter function for currency values.
|
|
153
|
+
*/
|
|
154
|
+
export function globalizeCurrencyFormatter(
|
|
155
|
+
currencyCode: string,
|
|
156
|
+
format: Globalize.CurrencyFormatterOptions,
|
|
157
|
+
): (num: number) => string {
|
|
158
|
+
return Globalize.currencyFormatter(currencyCode, format);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Formats a number using Globalize, handling errors gracefully.
|
|
163
|
+
*
|
|
164
|
+
* @param format - Globalize number formatter options.
|
|
165
|
+
* @param num - The number to format.
|
|
166
|
+
* @returns The formatted number as a string.
|
|
167
|
+
*/
|
|
168
|
+
export function formatNumberSafely<
|
|
169
|
+
FormatOptions extends Globalize.NumberFormatterOptions,
|
|
170
|
+
>(format: FormatOptions, num: number): string {
|
|
171
|
+
try {
|
|
172
|
+
const formatter = globalizeNumberFormatter(format);
|
|
173
|
+
const formattedNumber = formatter(num);
|
|
174
|
+
return formattedNumber;
|
|
175
|
+
} catch (e) {
|
|
176
|
+
logger.error('Error formatting pattern: ', format, num, e);
|
|
177
|
+
if (Math.abs(num) < 1e-7) {
|
|
178
|
+
return '0';
|
|
179
|
+
}
|
|
180
|
+
return String(num);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Sanitizes a number format to ensure compatibility with Globalize.
|
|
186
|
+
*
|
|
187
|
+
* @param format - The raw format string (e.g., '#.##').
|
|
188
|
+
* @returns A sanitized format string (e.g., '0.##').
|
|
189
|
+
*/
|
|
190
|
+
export const sanitizeFormat = (format: string): string => {
|
|
191
|
+
// Globalize needs to have a zero before the decimal point
|
|
192
|
+
// or at the end of format if no decimal point
|
|
193
|
+
let sanitizedFormat = format.replace(/#\./, '0.');
|
|
194
|
+
if (!sanitizedFormat.includes('.')) {
|
|
195
|
+
sanitizedFormat = sanitizedFormat.replace(/#(%?)$/, '0$1');
|
|
196
|
+
}
|
|
197
|
+
return sanitizedFormat;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Validates if a given number format is compatible with Globalize.
|
|
202
|
+
*
|
|
203
|
+
* @param format - The raw format string.
|
|
204
|
+
* @returns True if the format is valid; otherwise, false.
|
|
205
|
+
*/
|
|
206
|
+
export const validateNumberFormat = (format: string): boolean => {
|
|
207
|
+
try {
|
|
208
|
+
Globalize.numberFormatter({
|
|
209
|
+
...({ raw: sanitizeFormat(format) } as any),
|
|
210
|
+
})(123); // Test the formatter with a dummy value
|
|
211
|
+
} catch (e) {
|
|
212
|
+
logger.error('Invalid number format:', format, e);
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
return true;
|
|
216
|
+
};
|