@eternl/formats 0.1.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 +132 -0
- package/dist/cjs/constants.js +21 -0
- package/dist/cjs/csv.js +83 -0
- package/dist/cjs/date.js +61 -0
- package/dist/cjs/display.js +213 -0
- package/dist/cjs/errors.js +48 -0
- package/dist/cjs/index.js +22 -0
- package/dist/cjs/internal/intl-cache.js +92 -0
- package/dist/cjs/internal/numeric.js +164 -0
- package/dist/cjs/types.js +16 -0
- package/dist/esm/constants.js +19 -0
- package/dist/esm/constants.js.map +1 -0
- package/dist/esm/csv.js +77 -0
- package/dist/esm/csv.js.map +1 -0
- package/dist/esm/date.js +57 -0
- package/dist/esm/date.js.map +1 -0
- package/dist/esm/display.js +204 -0
- package/dist/esm/display.js.map +1 -0
- package/dist/esm/errors.js +42 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/intl-cache.js +87 -0
- package/dist/esm/internal/intl-cache.js.map +1 -0
- package/dist/esm/internal/numeric.js +154 -0
- package/dist/esm/internal/numeric.js.map +1 -0
- package/dist/esm/types.js +14 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/types/constants.d.ts +19 -0
- package/dist/types/csv.d.ts +5 -0
- package/dist/types/date.d.ts +2 -0
- package/dist/types/display.d.ts +9 -0
- package/dist/types/errors.d.ts +21 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/internal/intl-cache.d.ts +17 -0
- package/dist/types/internal/numeric.d.ts +19 -0
- package/dist/types/types.d.ts +50 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# @eternl/formats
|
|
2
|
+
|
|
3
|
+
Locale-aware display formatting and CSV-safe number helpers for browser-based dashboards and exports.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @eternl/formats
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import Big from 'big.js'
|
|
15
|
+
import {
|
|
16
|
+
buildCsv,
|
|
17
|
+
csvCurrencyCode,
|
|
18
|
+
formatCsvDate,
|
|
19
|
+
formatCsvNumber,
|
|
20
|
+
formatCsvPercent,
|
|
21
|
+
formatDisplayCurrency,
|
|
22
|
+
formatDisplayPercent,
|
|
23
|
+
formatDisplayCompact
|
|
24
|
+
} from '@eternl/formats'
|
|
25
|
+
|
|
26
|
+
const amount = new Big('1234567.890123')
|
|
27
|
+
|
|
28
|
+
const priceUS = formatDisplayCurrency(amount, {
|
|
29
|
+
locale: 'en-US',
|
|
30
|
+
currencyCode: 'USD',
|
|
31
|
+
minDecimals: 2,
|
|
32
|
+
maxDecimals: 2,
|
|
33
|
+
useSymbol: true
|
|
34
|
+
}) // "$1,234,567.89"
|
|
35
|
+
|
|
36
|
+
const priceDE = formatDisplayCurrency(amount, {
|
|
37
|
+
locale: 'de-DE',
|
|
38
|
+
currencyCode: 'EUR',
|
|
39
|
+
minDecimals: 2,
|
|
40
|
+
maxDecimals: 2,
|
|
41
|
+
useSymbol: true
|
|
42
|
+
}) // "1.234.567,89 €"
|
|
43
|
+
|
|
44
|
+
const adaDisplay = formatDisplayCurrency('123456789.123456', {
|
|
45
|
+
locale: 'en-US',
|
|
46
|
+
currencyCode: 'ADA',
|
|
47
|
+
customSymbols: { ADA: '₳' },
|
|
48
|
+
minDecimals: 2,
|
|
49
|
+
maxDecimals: 6
|
|
50
|
+
}) // "₳123,456,789.123456"
|
|
51
|
+
|
|
52
|
+
const pctDisplay = formatDisplayPercent('0.125', {
|
|
53
|
+
locale: 'en-US',
|
|
54
|
+
minDecimals: 1,
|
|
55
|
+
maxDecimals: 2
|
|
56
|
+
}) // "12.5%"
|
|
57
|
+
|
|
58
|
+
const compact = formatDisplayCompact('9876543', {
|
|
59
|
+
locale: 'en-US',
|
|
60
|
+
compact: true,
|
|
61
|
+
maxDecimals: 1
|
|
62
|
+
}) // "9.9M"
|
|
63
|
+
|
|
64
|
+
const csvNumber = formatCsvNumber(amount, { maxDecimals: 6, trimTrailingZeros: true })
|
|
65
|
+
const csvPercent = formatCsvPercent('0.125')
|
|
66
|
+
const csvCurrency = csvCurrencyCode('ADA', { ADA: '₳' }, false)
|
|
67
|
+
|
|
68
|
+
const rows = [
|
|
69
|
+
{ ts: new Date('2025-10-06T12:00:00Z'), amount, currency: 'ADA', ratio: '0.125' }
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
const csv = buildCsv(rows, [
|
|
73
|
+
{ header: 'date', accessor: (row) => row.ts, formatter: (value) => formatCsvDate(value as Date) },
|
|
74
|
+
{ header: 'amount', accessor: (row) => row.amount, formatter: (value) => formatCsvNumber(value as Big, { maxDecimals: 6 }) },
|
|
75
|
+
{ header: 'currency', accessor: (row) => row.currency, formatter: (value) => csvCurrencyCode(String(value), { ADA: '₳' }, false) },
|
|
76
|
+
{ header: 'percent', accessor: (row) => row.ratio, formatter: (value) => formatCsvPercent(value as string, { maxDecimals: 4 }) }
|
|
77
|
+
])
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The generated CSV is fully ASCII (`.` decimal separator, no grouping) and imports cleanly into Google Sheets, Koinly, and CoinTracking.
|
|
81
|
+
|
|
82
|
+
## Display Formatting
|
|
83
|
+
|
|
84
|
+
- `formatDisplayNumber` handles localized decimals with grouping, optional sign display, and rounding controls.
|
|
85
|
+
- `formatDisplayCurrency` combines `Intl.NumberFormat` with optional custom symbols (₳, ₿, etc.). Spacing from the host locale is preserved unless a custom symbol removes it.
|
|
86
|
+
- `formatDisplayPercent` multiplies by 100 for display while respecting locale decimal and percent symbols.
|
|
87
|
+
- `formatDisplayCompact` applies configurable K/M/B/T suffixes using precise `big.js` arithmetic (no scientific notation).
|
|
88
|
+
|
|
89
|
+
Common options:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
{
|
|
93
|
+
minDecimals?: number
|
|
94
|
+
maxDecimals?: number
|
|
95
|
+
precision?: number // significant digits
|
|
96
|
+
roundingMode?: 'roundDown' | 'roundHalfUp' | 'roundHalfEven' | 'roundUp'
|
|
97
|
+
trimTrailingZeros?: boolean
|
|
98
|
+
signDisplay?: 'auto' | 'always' | 'exceptZero' | 'never' | 'negative'
|
|
99
|
+
useGrouping?: boolean
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## CSV Formatting
|
|
104
|
+
|
|
105
|
+
- `formatCsvNumber` outputs plain ASCII digits and a dot decimal separator, honoring min/max decimals, precision, and trimming.
|
|
106
|
+
- `formatCsvPercent` multiplies the ratio by 100 and emits an unadorned numeric string (no `%`).
|
|
107
|
+
- `csvCurrencyCode` keeps currency symbols or codes in a dedicated column so numeric fields stay clean.
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
formatCsvNumber('0.00000001', { maxDecimals: 8 }) // "0.00000001"
|
|
111
|
+
formatCsvPercent('0.125', { maxDecimals: 4 }) // "12.5"
|
|
112
|
+
csvCurrencyCode('btc', { BTC: '₿' }, true) // "₿"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## CSV Builder
|
|
116
|
+
|
|
117
|
+
`buildCsv(rows, columns, delimiter = ',', lineEnding = '\n')` applies per-column accessors and formatters, quoting fields with delimiters, quotes, or newlines (RFC 4180). Numeric results produced by `formatCsvNumber` never require quoting, so imports remain numeric.
|
|
118
|
+
|
|
119
|
+
## Date Helpers
|
|
120
|
+
|
|
121
|
+
- `formatDisplayDate(input, locale, options)` caches `Intl.DateTimeFormat` instances.
|
|
122
|
+
- `formatCsvDate(input)` emits ISO-8601 strings: `YYYY-MM-DD` for midnight UTC, otherwise `YYYY-MM-DDTHH:mm:ssZ`.
|
|
123
|
+
|
|
124
|
+
## Development
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
npm run lint # type-check
|
|
128
|
+
npm test # run Vitest suite
|
|
129
|
+
npm run build # emit ESM + CJS bundles into dist/
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The test suite covers locale differences, rounding edge cases, percent handling, large/small magnitudes, and CSV quoting rules.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DECIMAL_DOT = exports.DECIMAL_EMPTY = exports.DECIMAL_ZERO = exports.DEFAULT_SIGN_PLUS = exports.DEFAULT_SIGN_MINUS = exports.COMPACT_ORDER = exports.DISPLAY_SIGN_NONE = exports.DISPLAY_SIGN_PLUS = exports.DISPLAY_SIGN_MINUS = exports.DISPLAY_SIGNS = exports.STYLE_PERCENT = exports.STYLE_CURRENCY = exports.STYLE_DECIMAL = void 0;
|
|
4
|
+
const types_1 = require("./types");
|
|
5
|
+
exports.STYLE_DECIMAL = types_1.NUMBER_STYLES.decimal;
|
|
6
|
+
exports.STYLE_CURRENCY = types_1.NUMBER_STYLES.currency;
|
|
7
|
+
exports.STYLE_PERCENT = types_1.NUMBER_STYLES.percent;
|
|
8
|
+
exports.DISPLAY_SIGNS = {
|
|
9
|
+
minus: 'minus',
|
|
10
|
+
plus: 'plus',
|
|
11
|
+
none: 'none'
|
|
12
|
+
};
|
|
13
|
+
exports.DISPLAY_SIGN_MINUS = exports.DISPLAY_SIGNS.minus;
|
|
14
|
+
exports.DISPLAY_SIGN_PLUS = exports.DISPLAY_SIGNS.plus;
|
|
15
|
+
exports.DISPLAY_SIGN_NONE = exports.DISPLAY_SIGNS.none;
|
|
16
|
+
exports.COMPACT_ORDER = [...types_1.COMPACT_UNITS].reverse();
|
|
17
|
+
exports.DEFAULT_SIGN_MINUS = '-';
|
|
18
|
+
exports.DEFAULT_SIGN_PLUS = '+';
|
|
19
|
+
exports.DECIMAL_ZERO = '0';
|
|
20
|
+
exports.DECIMAL_EMPTY = '';
|
|
21
|
+
exports.DECIMAL_DOT = '.';
|
package/dist/cjs/csv.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildCsv = exports.csvCurrencyCode = exports.formatCsvPercent = exports.formatCsvNumber = void 0;
|
|
4
|
+
const constants_1 = require("./constants");
|
|
5
|
+
const date_1 = require("./date");
|
|
6
|
+
const numeric_1 = require("./internal/numeric");
|
|
7
|
+
const stringifyNumericParts = (parts) => {
|
|
8
|
+
const fraction = parts.fraction.length > 0 ? `${constants_1.DECIMAL_DOT}${parts.fraction}` : constants_1.DECIMAL_EMPTY;
|
|
9
|
+
const integer = parts.integer || constants_1.DECIMAL_ZERO;
|
|
10
|
+
const prefix = parts.isNegative ? constants_1.DEFAULT_SIGN_MINUS : constants_1.DECIMAL_EMPTY;
|
|
11
|
+
const value = `${prefix}${integer}${fraction}`;
|
|
12
|
+
return value;
|
|
13
|
+
};
|
|
14
|
+
const formatCsvNumber = (value, options = {}) => {
|
|
15
|
+
const parts = (0, numeric_1.computeNumericParts)(value, options);
|
|
16
|
+
return stringifyNumericParts(parts);
|
|
17
|
+
};
|
|
18
|
+
exports.formatCsvNumber = formatCsvNumber;
|
|
19
|
+
const formatCsvPercent = (value, options = {}) => {
|
|
20
|
+
const scaled = (0, numeric_1.normalizeNumeric)(value).times(100);
|
|
21
|
+
const parts = (0, numeric_1.computeNumericParts)(scaled, options);
|
|
22
|
+
return stringifyNumericParts(parts);
|
|
23
|
+
};
|
|
24
|
+
exports.formatCsvPercent = formatCsvPercent;
|
|
25
|
+
const csvCurrencyCode = (code, customSymbols, useSymbol) => {
|
|
26
|
+
const trimmed = code.trim();
|
|
27
|
+
if (!trimmed) {
|
|
28
|
+
return constants_1.DECIMAL_EMPTY;
|
|
29
|
+
}
|
|
30
|
+
const normalized = trimmed.toUpperCase();
|
|
31
|
+
if (useSymbol) {
|
|
32
|
+
const symbol = customSymbols?.[normalized];
|
|
33
|
+
if (symbol) {
|
|
34
|
+
return symbol;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return normalized;
|
|
38
|
+
};
|
|
39
|
+
exports.csvCurrencyCode = csvCurrencyCode;
|
|
40
|
+
const escapeCsvField = (value, delimiter) => {
|
|
41
|
+
const needsQuoting = value.includes(delimiter)
|
|
42
|
+
|| value.includes('\n')
|
|
43
|
+
|| value.includes('\r')
|
|
44
|
+
|| value.includes('"');
|
|
45
|
+
if (!needsQuoting) {
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
const escaped = value.replace(/"/g, '""');
|
|
49
|
+
return `"${escaped}"`;
|
|
50
|
+
};
|
|
51
|
+
const defaultFormatter = (value) => {
|
|
52
|
+
if (value === null || value === undefined) {
|
|
53
|
+
return constants_1.DECIMAL_EMPTY;
|
|
54
|
+
}
|
|
55
|
+
if (value instanceof Date) {
|
|
56
|
+
return (0, date_1.formatCsvDate)(value);
|
|
57
|
+
}
|
|
58
|
+
if (typeof value === 'number') {
|
|
59
|
+
return (0, exports.formatCsvNumber)(value);
|
|
60
|
+
}
|
|
61
|
+
if (typeof value === 'string') {
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
if (typeof value === 'boolean') {
|
|
65
|
+
return value ? 'true' : 'false';
|
|
66
|
+
}
|
|
67
|
+
return String(value);
|
|
68
|
+
};
|
|
69
|
+
const buildCsv = (rows, columns, delimiter = ',', lineEnding = '\n') => {
|
|
70
|
+
const headerLine = columns.map((col) => escapeCsvField(col.header, delimiter)).join(delimiter);
|
|
71
|
+
const lines = [headerLine];
|
|
72
|
+
for (const row of rows) {
|
|
73
|
+
const fields = columns.map((col) => {
|
|
74
|
+
const raw = col.accessor(row);
|
|
75
|
+
const formatted = col.formatter ? col.formatter(raw) : defaultFormatter(raw);
|
|
76
|
+
const stringValue = formatted ?? constants_1.DECIMAL_EMPTY;
|
|
77
|
+
return escapeCsvField(stringValue, delimiter);
|
|
78
|
+
});
|
|
79
|
+
lines.push(fields.join(delimiter));
|
|
80
|
+
}
|
|
81
|
+
return `${lines.join(lineEnding)}${lineEnding}`;
|
|
82
|
+
};
|
|
83
|
+
exports.buildCsv = buildCsv;
|
package/dist/cjs/date.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatCsvDate = exports.formatDisplayDate = void 0;
|
|
4
|
+
const errors_1 = require("./errors");
|
|
5
|
+
const intl_cache_1 = require("./internal/intl-cache");
|
|
6
|
+
const toDate = (value) => {
|
|
7
|
+
if (value instanceof Date) {
|
|
8
|
+
const timestamp = value.getTime();
|
|
9
|
+
if (Number.isNaN(timestamp)) {
|
|
10
|
+
throw (0, errors_1.toRangeError)(errors_1.FormatError.DateValueInvalid);
|
|
11
|
+
}
|
|
12
|
+
return new Date(timestamp);
|
|
13
|
+
}
|
|
14
|
+
if (typeof value === 'number') {
|
|
15
|
+
if (!Number.isFinite(value)) {
|
|
16
|
+
throw (0, errors_1.toRangeError)(errors_1.FormatError.DateNumberNonFinite);
|
|
17
|
+
}
|
|
18
|
+
const date = new Date(value);
|
|
19
|
+
if (Number.isNaN(date.getTime())) {
|
|
20
|
+
throw (0, errors_1.toRangeError)(errors_1.FormatError.DateValueInvalid);
|
|
21
|
+
}
|
|
22
|
+
return date;
|
|
23
|
+
}
|
|
24
|
+
if (typeof value === 'string') {
|
|
25
|
+
const trimmed = value.trim();
|
|
26
|
+
if (!trimmed) {
|
|
27
|
+
throw (0, errors_1.toRangeError)(errors_1.FormatError.DateStringEmpty);
|
|
28
|
+
}
|
|
29
|
+
const date = new Date(trimmed);
|
|
30
|
+
if (Number.isNaN(date.getTime())) {
|
|
31
|
+
throw (0, errors_1.toRangeError)(errors_1.FormatError.InvalidDateString);
|
|
32
|
+
}
|
|
33
|
+
return date;
|
|
34
|
+
}
|
|
35
|
+
throw (0, errors_1.toTypeError)(errors_1.FormatError.UnsupportedDateInput);
|
|
36
|
+
};
|
|
37
|
+
const formatDisplayDate = (date, locale, options) => {
|
|
38
|
+
if (!locale) {
|
|
39
|
+
throw (0, errors_1.toError)(errors_1.FormatError.MissingLocale);
|
|
40
|
+
}
|
|
41
|
+
const parsed = toDate(date);
|
|
42
|
+
const formatter = (0, intl_cache_1.getDateFormat)(locale, options);
|
|
43
|
+
return formatter.format(parsed);
|
|
44
|
+
};
|
|
45
|
+
exports.formatDisplayDate = formatDisplayDate;
|
|
46
|
+
const formatCsvDate = (date) => {
|
|
47
|
+
const parsed = toDate(date);
|
|
48
|
+
const iso = parsed.toISOString();
|
|
49
|
+
const datePart = iso.slice(0, 10);
|
|
50
|
+
const hasTime = parsed.getUTCHours() !== 0
|
|
51
|
+
|| parsed.getUTCMinutes() !== 0
|
|
52
|
+
|| parsed.getUTCSeconds() !== 0
|
|
53
|
+
|| parsed.getUTCMilliseconds() !== 0;
|
|
54
|
+
if (!hasTime) {
|
|
55
|
+
return datePart;
|
|
56
|
+
}
|
|
57
|
+
const timePart = iso.slice(11);
|
|
58
|
+
const withoutMillis = timePart.replace(/\.\d+Z$/, 'Z');
|
|
59
|
+
return `${datePart}T${withoutMillis}`;
|
|
60
|
+
};
|
|
61
|
+
exports.formatCsvDate = formatCsvDate;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.formatDisplayCompact = exports.formatDisplayPercent = exports.formatDisplayCurrency = exports.formatDisplayNumber = void 0;
|
|
7
|
+
const big_js_1 = __importDefault(require("big.js"));
|
|
8
|
+
const numeric_1 = require("./internal/numeric");
|
|
9
|
+
const intl_cache_1 = require("./internal/intl-cache");
|
|
10
|
+
const errors_1 = require("./errors");
|
|
11
|
+
const constants_1 = require("./constants");
|
|
12
|
+
const types_1 = require("./types");
|
|
13
|
+
const DEFAULT_COMPACT_THRESHOLDS = {
|
|
14
|
+
K: new big_js_1.default(1000),
|
|
15
|
+
M: new big_js_1.default(1000000),
|
|
16
|
+
B: new big_js_1.default(1000000000),
|
|
17
|
+
T: new big_js_1.default(1000000000000)
|
|
18
|
+
};
|
|
19
|
+
const DEFAULT_COMPACT_SUFFIXES = {
|
|
20
|
+
K: 'K',
|
|
21
|
+
M: 'M',
|
|
22
|
+
B: 'B',
|
|
23
|
+
T: 'T'
|
|
24
|
+
};
|
|
25
|
+
const resolveSignKind = (isNegative, isZero, signDisplay) => {
|
|
26
|
+
const mode = signDisplay ?? types_1.SIGN_DISPLAYS.auto;
|
|
27
|
+
switch (mode) {
|
|
28
|
+
case types_1.SIGN_DISPLAYS.always: return isNegative ? constants_1.DISPLAY_SIGN_MINUS : constants_1.DISPLAY_SIGN_PLUS;
|
|
29
|
+
case types_1.SIGN_DISPLAYS.exceptZero: return isNegative ? constants_1.DISPLAY_SIGN_MINUS : (isZero ? constants_1.DISPLAY_SIGN_NONE : constants_1.DISPLAY_SIGN_PLUS);
|
|
30
|
+
case types_1.SIGN_DISPLAYS.never: return constants_1.DISPLAY_SIGN_NONE;
|
|
31
|
+
case types_1.SIGN_DISPLAYS.negative: return isNegative ? constants_1.DISPLAY_SIGN_MINUS : constants_1.DISPLAY_SIGN_NONE;
|
|
32
|
+
case types_1.SIGN_DISPLAYS.auto:
|
|
33
|
+
default: return isNegative ? constants_1.DISPLAY_SIGN_MINUS : constants_1.DISPLAY_SIGN_NONE;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const resolveCurrencyToken = (template, options) => {
|
|
37
|
+
const code = options.currencyCode?.toUpperCase() ?? '';
|
|
38
|
+
if (options.useSymbol === false) {
|
|
39
|
+
return { value: code, fromCustom: false };
|
|
40
|
+
}
|
|
41
|
+
const custom = code ? options.customSymbols?.[code] : undefined;
|
|
42
|
+
if (custom) {
|
|
43
|
+
return { value: custom, fromCustom: true };
|
|
44
|
+
}
|
|
45
|
+
if (template && template !== code) {
|
|
46
|
+
return { value: template, fromCustom: false };
|
|
47
|
+
}
|
|
48
|
+
return { value: code, fromCustom: false };
|
|
49
|
+
};
|
|
50
|
+
const resolveCompact = (value, options) => {
|
|
51
|
+
if (!options.compact) {
|
|
52
|
+
return { value };
|
|
53
|
+
}
|
|
54
|
+
const abs = value.abs();
|
|
55
|
+
let chosen;
|
|
56
|
+
for (const unit of constants_1.COMPACT_ORDER) {
|
|
57
|
+
const overrideThreshold = options.compactThresholds?.[unit];
|
|
58
|
+
const threshold = overrideThreshold !== undefined
|
|
59
|
+
? (0, numeric_1.normalizeNumeric)(overrideThreshold)
|
|
60
|
+
: DEFAULT_COMPACT_THRESHOLDS[unit];
|
|
61
|
+
if (abs.gte(threshold)) {
|
|
62
|
+
chosen = unit;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (!chosen) {
|
|
67
|
+
return { value };
|
|
68
|
+
}
|
|
69
|
+
const threshold = options.compactThresholds?.[chosen]
|
|
70
|
+
? (0, numeric_1.normalizeNumeric)(options.compactThresholds[chosen])
|
|
71
|
+
: DEFAULT_COMPACT_THRESHOLDS[chosen];
|
|
72
|
+
const suffix = options.compactSuffixes?.[chosen] ?? DEFAULT_COMPACT_SUFFIXES[chosen];
|
|
73
|
+
const scaled = threshold.eq(0) ? value : value.div(threshold);
|
|
74
|
+
return { value: scaled, suffix };
|
|
75
|
+
};
|
|
76
|
+
const buildNumberFormatOptions = (options) => {
|
|
77
|
+
const nfOptions = {
|
|
78
|
+
style: options.style,
|
|
79
|
+
useGrouping: options.useGrouping !== false
|
|
80
|
+
};
|
|
81
|
+
if (options.signDisplay && options.signDisplay !== types_1.SIGN_DISPLAYS.negative) {
|
|
82
|
+
nfOptions.signDisplay = options.signDisplay;
|
|
83
|
+
}
|
|
84
|
+
if (options.minDecimals !== undefined) {
|
|
85
|
+
nfOptions.minimumFractionDigits = options.minDecimals;
|
|
86
|
+
}
|
|
87
|
+
if (options.maxDecimals !== undefined) {
|
|
88
|
+
nfOptions.maximumFractionDigits = options.maxDecimals;
|
|
89
|
+
}
|
|
90
|
+
if (options.style === constants_1.STYLE_CURRENCY && options.currencyCode) {
|
|
91
|
+
nfOptions.currency = options.currencyCode;
|
|
92
|
+
nfOptions.currencyDisplay = options.useSymbol === false ? 'code' : 'symbol';
|
|
93
|
+
}
|
|
94
|
+
return nfOptions;
|
|
95
|
+
};
|
|
96
|
+
const formatFromPattern = (options, integerPart, fractionPart, sign, suffix) => {
|
|
97
|
+
const nfOptions = buildNumberFormatOptions(options);
|
|
98
|
+
const pattern = (0, intl_cache_1.getNumberFormatPattern)(options.locale, nfOptions);
|
|
99
|
+
const parts = sign === constants_1.DISPLAY_SIGN_MINUS ? pattern.negative : pattern.positive;
|
|
100
|
+
const useGrouping = options.useGrouping !== false;
|
|
101
|
+
const groupedInteger = (0, numeric_1.applyGrouping)(integerPart, {
|
|
102
|
+
separator: pattern.groupSeparator,
|
|
103
|
+
primary: pattern.grouping.primary,
|
|
104
|
+
secondary: pattern.grouping.secondary
|
|
105
|
+
}, useGrouping);
|
|
106
|
+
const hasFraction = fractionPart.length > 0;
|
|
107
|
+
let result = '';
|
|
108
|
+
let integerInjected = false;
|
|
109
|
+
let currencyMeta;
|
|
110
|
+
let previousType;
|
|
111
|
+
for (const part of parts) {
|
|
112
|
+
switch (part.type) {
|
|
113
|
+
case 'integer': {
|
|
114
|
+
if (!integerInjected) {
|
|
115
|
+
result += groupedInteger;
|
|
116
|
+
integerInjected = true;
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
case 'group':
|
|
121
|
+
break;
|
|
122
|
+
case 'decimal': {
|
|
123
|
+
if (hasFraction) {
|
|
124
|
+
result += pattern.decimalSeparator;
|
|
125
|
+
}
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
case 'fraction': {
|
|
129
|
+
if (hasFraction) {
|
|
130
|
+
result += fractionPart;
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
case 'minusSign': {
|
|
135
|
+
if (sign === constants_1.DISPLAY_SIGN_MINUS) {
|
|
136
|
+
result += pattern.minusSign ?? part.value ?? constants_1.DEFAULT_SIGN_MINUS;
|
|
137
|
+
}
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
case 'plusSign': {
|
|
141
|
+
if (sign === constants_1.DISPLAY_SIGN_PLUS) {
|
|
142
|
+
result += pattern.plusSign ?? part.value ?? constants_1.DEFAULT_SIGN_PLUS;
|
|
143
|
+
}
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
case 'currency': {
|
|
147
|
+
currencyMeta = currencyMeta ?? resolveCurrencyToken(part.value, options);
|
|
148
|
+
result += currencyMeta.value;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
case 'percentSign': {
|
|
152
|
+
result += part.value;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
case 'literal': {
|
|
156
|
+
if (previousType === 'currency' && currencyMeta?.fromCustom && /^\s+$/.test(part.value)) {
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
result += part.value;
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
default: {
|
|
163
|
+
result += part.value;
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
previousType = part.type;
|
|
168
|
+
}
|
|
169
|
+
if (suffix) {
|
|
170
|
+
result += suffix;
|
|
171
|
+
}
|
|
172
|
+
return result;
|
|
173
|
+
};
|
|
174
|
+
const resolveDisplayOptions = (options, style) => {
|
|
175
|
+
if (!options.locale) {
|
|
176
|
+
throw (0, errors_1.toError)(errors_1.FormatError.MissingLocale);
|
|
177
|
+
}
|
|
178
|
+
if (style === constants_1.STYLE_CURRENCY && !options.currencyCode) {
|
|
179
|
+
throw (0, errors_1.toError)(errors_1.FormatError.MissingCurrencyCode);
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
...options,
|
|
183
|
+
style
|
|
184
|
+
};
|
|
185
|
+
};
|
|
186
|
+
const formatDisplayInternal = (value, options, style) => {
|
|
187
|
+
const resolved = resolveDisplayOptions(options, style);
|
|
188
|
+
const baseValue = style === constants_1.STYLE_PERCENT
|
|
189
|
+
? (0, numeric_1.normalizeNumeric)(value).times(100)
|
|
190
|
+
: (0, numeric_1.normalizeNumeric)(value);
|
|
191
|
+
const { value: compactValue, suffix } = resolveCompact(baseValue, resolved);
|
|
192
|
+
const numericParts = (0, numeric_1.computeNumericParts)(compactValue, resolved);
|
|
193
|
+
const sign = resolveSignKind(numericParts.isNegative, numericParts.isZero, resolved.signDisplay);
|
|
194
|
+
return formatFromPattern(resolved, numericParts.integer, numericParts.fraction, sign, suffix);
|
|
195
|
+
};
|
|
196
|
+
const formatDisplayNumber = (value, options) => {
|
|
197
|
+
const style = options.style ?? constants_1.STYLE_DECIMAL;
|
|
198
|
+
return formatDisplayInternal(value, { ...options, style }, style);
|
|
199
|
+
};
|
|
200
|
+
exports.formatDisplayNumber = formatDisplayNumber;
|
|
201
|
+
const formatDisplayCurrency = (value, options) => {
|
|
202
|
+
return formatDisplayInternal(value, { ...options, style: constants_1.STYLE_CURRENCY }, constants_1.STYLE_CURRENCY);
|
|
203
|
+
};
|
|
204
|
+
exports.formatDisplayCurrency = formatDisplayCurrency;
|
|
205
|
+
const formatDisplayPercent = (value, options) => {
|
|
206
|
+
return formatDisplayInternal(value, { ...options, style: constants_1.STYLE_PERCENT }, constants_1.STYLE_PERCENT);
|
|
207
|
+
};
|
|
208
|
+
exports.formatDisplayPercent = formatDisplayPercent;
|
|
209
|
+
const formatDisplayCompact = (value, options) => {
|
|
210
|
+
const style = options.style ?? constants_1.STYLE_DECIMAL;
|
|
211
|
+
return formatDisplayInternal(value, { ...options, compact: true, style }, style);
|
|
212
|
+
};
|
|
213
|
+
exports.formatDisplayCompact = formatDisplayCompact;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toError = exports.toTypeError = exports.toRangeError = exports.getFormatErrorMessage = exports.FormatError = void 0;
|
|
4
|
+
var FormatError;
|
|
5
|
+
(function (FormatError) {
|
|
6
|
+
FormatError["NonNegativeIntegerRequired"] = "nonNegativeIntegerRequired";
|
|
7
|
+
FormatError["PositiveIntegerRequired"] = "positiveIntegerRequired";
|
|
8
|
+
FormatError["NumericValueNonFinite"] = "numericValueNonFinite";
|
|
9
|
+
FormatError["NumericStringEmpty"] = "numericStringEmpty";
|
|
10
|
+
FormatError["UnsupportedNumericInput"] = "unsupportedNumericInput";
|
|
11
|
+
FormatError["MaxDecimalsLessThanMinimum"] = "maxDecimalsLessThanMinimum";
|
|
12
|
+
FormatError["MissingLocale"] = "missingLocale";
|
|
13
|
+
FormatError["MissingCurrencyCode"] = "missingCurrencyCode";
|
|
14
|
+
FormatError["DateValueInvalid"] = "dateValueInvalid";
|
|
15
|
+
FormatError["DateStringEmpty"] = "dateStringEmpty";
|
|
16
|
+
FormatError["DateNumberNonFinite"] = "dateNumberNonFinite";
|
|
17
|
+
FormatError["InvalidDateString"] = "invalidDateString";
|
|
18
|
+
FormatError["UnsupportedDateInput"] = "unsupportedDateInput";
|
|
19
|
+
})(FormatError || (exports.FormatError = FormatError = {}));
|
|
20
|
+
const FORMAT_ERROR_MESSAGES = {
|
|
21
|
+
[FormatError.NonNegativeIntegerRequired]: '%label% must be a non-negative integer',
|
|
22
|
+
[FormatError.PositiveIntegerRequired]: '%label% must be a positive integer',
|
|
23
|
+
[FormatError.NumericValueNonFinite]: 'Numeric value must be a finite number',
|
|
24
|
+
[FormatError.NumericStringEmpty]: 'Numeric string cannot be empty',
|
|
25
|
+
[FormatError.UnsupportedNumericInput]: 'Unsupported numeric input type',
|
|
26
|
+
[FormatError.MaxDecimalsLessThanMinimum]: 'maxDecimals cannot be less than minDecimals',
|
|
27
|
+
[FormatError.MissingLocale]: 'Display formatting requires a locale',
|
|
28
|
+
[FormatError.MissingCurrencyCode]: 'Currency formatting requires a currencyCode',
|
|
29
|
+
[FormatError.DateValueInvalid]: 'Invalid date value',
|
|
30
|
+
[FormatError.DateStringEmpty]: 'Date string cannot be empty',
|
|
31
|
+
[FormatError.DateNumberNonFinite]: 'Date number must be finite',
|
|
32
|
+
[FormatError.InvalidDateString]: 'Invalid date string',
|
|
33
|
+
[FormatError.UnsupportedDateInput]: 'Unsupported date input type'
|
|
34
|
+
};
|
|
35
|
+
const applyDetails = (template, details) => {
|
|
36
|
+
if (!details) {
|
|
37
|
+
return template;
|
|
38
|
+
}
|
|
39
|
+
return Object.keys(details).reduce((message, key) => message.replace(new RegExp(`%${key}%`, 'g'), details[key]), template);
|
|
40
|
+
};
|
|
41
|
+
const getFormatErrorMessage = (code, details) => applyDetails(FORMAT_ERROR_MESSAGES[code], details);
|
|
42
|
+
exports.getFormatErrorMessage = getFormatErrorMessage;
|
|
43
|
+
const toRangeError = (code, details) => new RangeError((0, exports.getFormatErrorMessage)(code, details));
|
|
44
|
+
exports.toRangeError = toRangeError;
|
|
45
|
+
const toTypeError = (code, details) => new TypeError((0, exports.getFormatErrorMessage)(code, details));
|
|
46
|
+
exports.toTypeError = toTypeError;
|
|
47
|
+
const toError = (code, details) => new Error((0, exports.getFormatErrorMessage)(code, details));
|
|
48
|
+
exports.toError = toError;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toTypeError = exports.toRangeError = exports.toError = exports.getFormatErrorMessage = exports.FormatError = exports.formatCsvDate = exports.formatDisplayDate = exports.buildCsv = exports.csvCurrencyCode = exports.formatCsvPercent = exports.formatCsvNumber = exports.formatDisplayCompact = exports.formatDisplayPercent = exports.formatDisplayCurrency = exports.formatDisplayNumber = void 0;
|
|
4
|
+
var display_1 = require("./display");
|
|
5
|
+
Object.defineProperty(exports, "formatDisplayNumber", { enumerable: true, get: function () { return display_1.formatDisplayNumber; } });
|
|
6
|
+
Object.defineProperty(exports, "formatDisplayCurrency", { enumerable: true, get: function () { return display_1.formatDisplayCurrency; } });
|
|
7
|
+
Object.defineProperty(exports, "formatDisplayPercent", { enumerable: true, get: function () { return display_1.formatDisplayPercent; } });
|
|
8
|
+
Object.defineProperty(exports, "formatDisplayCompact", { enumerable: true, get: function () { return display_1.formatDisplayCompact; } });
|
|
9
|
+
var csv_1 = require("./csv");
|
|
10
|
+
Object.defineProperty(exports, "formatCsvNumber", { enumerable: true, get: function () { return csv_1.formatCsvNumber; } });
|
|
11
|
+
Object.defineProperty(exports, "formatCsvPercent", { enumerable: true, get: function () { return csv_1.formatCsvPercent; } });
|
|
12
|
+
Object.defineProperty(exports, "csvCurrencyCode", { enumerable: true, get: function () { return csv_1.csvCurrencyCode; } });
|
|
13
|
+
Object.defineProperty(exports, "buildCsv", { enumerable: true, get: function () { return csv_1.buildCsv; } });
|
|
14
|
+
var date_1 = require("./date");
|
|
15
|
+
Object.defineProperty(exports, "formatDisplayDate", { enumerable: true, get: function () { return date_1.formatDisplayDate; } });
|
|
16
|
+
Object.defineProperty(exports, "formatCsvDate", { enumerable: true, get: function () { return date_1.formatCsvDate; } });
|
|
17
|
+
var errors_1 = require("./errors");
|
|
18
|
+
Object.defineProperty(exports, "FormatError", { enumerable: true, get: function () { return errors_1.FormatError; } });
|
|
19
|
+
Object.defineProperty(exports, "getFormatErrorMessage", { enumerable: true, get: function () { return errors_1.getFormatErrorMessage; } });
|
|
20
|
+
Object.defineProperty(exports, "toError", { enumerable: true, get: function () { return errors_1.toError; } });
|
|
21
|
+
Object.defineProperty(exports, "toRangeError", { enumerable: true, get: function () { return errors_1.toRangeError; } });
|
|
22
|
+
Object.defineProperty(exports, "toTypeError", { enumerable: true, get: function () { return errors_1.toTypeError; } });
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getDateFormat = exports.getNumberFormatPattern = exports.getNumberFormat = void 0;
|
|
4
|
+
const numberFormatCache = new Map();
|
|
5
|
+
const numberFormatMetaCache = new Map();
|
|
6
|
+
const dateFormatCache = new Map();
|
|
7
|
+
const serializeOptions = (options) => {
|
|
8
|
+
const entries = Object.entries(options)
|
|
9
|
+
.filter(([, value]) => value !== undefined)
|
|
10
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
11
|
+
.map(([key, value]) => {
|
|
12
|
+
if (value
|
|
13
|
+
&& typeof value === 'object'
|
|
14
|
+
&& !Array.isArray(value)) {
|
|
15
|
+
return `${key}:{${serializeOptions(value)}}`;
|
|
16
|
+
}
|
|
17
|
+
if (Array.isArray(value)) {
|
|
18
|
+
return `${key}:[${value.join('|')}]`;
|
|
19
|
+
}
|
|
20
|
+
return `${key}:${String(value)}`;
|
|
21
|
+
});
|
|
22
|
+
return entries.join(',');
|
|
23
|
+
};
|
|
24
|
+
const buildNumberFormatKey = (locale, options) => `${locale}|${serializeOptions(options)}`;
|
|
25
|
+
const buildDateFormatKey = (locale, options) => `${locale}|${options ? serializeOptions(options) : ''}`;
|
|
26
|
+
const getNumberFormat = (locale, options) => {
|
|
27
|
+
const key = buildNumberFormatKey(locale, options);
|
|
28
|
+
const cached = numberFormatCache.get(key);
|
|
29
|
+
if (cached) {
|
|
30
|
+
return cached;
|
|
31
|
+
}
|
|
32
|
+
const nf = new Intl.NumberFormat(locale, options);
|
|
33
|
+
numberFormatCache.set(key, nf);
|
|
34
|
+
return nf;
|
|
35
|
+
};
|
|
36
|
+
exports.getNumberFormat = getNumberFormat;
|
|
37
|
+
const deriveGrouping = (parts) => {
|
|
38
|
+
const integerParts = parts.filter((part) => part.type === 'integer');
|
|
39
|
+
if (integerParts.length <= 1) {
|
|
40
|
+
return { primary: 0 };
|
|
41
|
+
}
|
|
42
|
+
const lengths = integerParts.map((part) => part.value.length);
|
|
43
|
+
const primary = lengths[lengths.length - 1];
|
|
44
|
+
const secondary = lengths.length > 1 ? lengths[lengths.length - 2] : undefined;
|
|
45
|
+
if (secondary === primary) {
|
|
46
|
+
return { primary };
|
|
47
|
+
}
|
|
48
|
+
return { primary, secondary };
|
|
49
|
+
};
|
|
50
|
+
const analyzePattern = (nf) => {
|
|
51
|
+
const sampleValue = 1234567.89;
|
|
52
|
+
const positiveParts = nf.formatToParts(sampleValue);
|
|
53
|
+
const negativeParts = nf.formatToParts(-sampleValue);
|
|
54
|
+
const plusProbe = nf.formatToParts(1);
|
|
55
|
+
const groupSeparator = positiveParts.find((part) => part.type === 'group')?.value ?? '';
|
|
56
|
+
const decimalSeparator = positiveParts.find((part) => part.type === 'decimal')?.value ?? '.';
|
|
57
|
+
const grouping = deriveGrouping(positiveParts);
|
|
58
|
+
const minusSign = negativeParts.find((part) => part.type === 'minusSign')?.value;
|
|
59
|
+
const plusSign = plusProbe.find((part) => part.type === 'plusSign')?.value;
|
|
60
|
+
return {
|
|
61
|
+
positive: positiveParts,
|
|
62
|
+
negative: negativeParts,
|
|
63
|
+
groupSeparator,
|
|
64
|
+
decimalSeparator,
|
|
65
|
+
grouping,
|
|
66
|
+
minusSign,
|
|
67
|
+
plusSign
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
const getNumberFormatPattern = (locale, options) => {
|
|
71
|
+
const key = buildNumberFormatKey(locale, options);
|
|
72
|
+
const cached = numberFormatMetaCache.get(key);
|
|
73
|
+
if (cached) {
|
|
74
|
+
return cached;
|
|
75
|
+
}
|
|
76
|
+
const nf = (0, exports.getNumberFormat)(locale, options);
|
|
77
|
+
const pattern = analyzePattern(nf);
|
|
78
|
+
numberFormatMetaCache.set(key, pattern);
|
|
79
|
+
return pattern;
|
|
80
|
+
};
|
|
81
|
+
exports.getNumberFormatPattern = getNumberFormatPattern;
|
|
82
|
+
const getDateFormat = (locale, options) => {
|
|
83
|
+
const key = buildDateFormatKey(locale, options);
|
|
84
|
+
const cached = dateFormatCache.get(key);
|
|
85
|
+
if (cached) {
|
|
86
|
+
return cached;
|
|
87
|
+
}
|
|
88
|
+
const df = new Intl.DateTimeFormat(locale, options);
|
|
89
|
+
dateFormatCache.set(key, df);
|
|
90
|
+
return df;
|
|
91
|
+
};
|
|
92
|
+
exports.getDateFormat = getDateFormat;
|