@formatjs/icu-skeleton-parser 1.3.10 → 1.3.11

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/number.ts ADDED
@@ -0,0 +1,357 @@
1
+ import type {NumberFormatOptions} from '@formatjs/ecma402-abstract'
2
+ import {WHITE_SPACE_REGEX} from './regex.generated'
3
+
4
+ export interface ExtendedNumberFormatOptions extends NumberFormatOptions {
5
+ scale?: number
6
+ }
7
+ export interface NumberSkeletonToken {
8
+ stem: string
9
+ options: string[]
10
+ }
11
+
12
+ export function parseNumberSkeletonFromString(
13
+ skeleton: string
14
+ ): NumberSkeletonToken[] {
15
+ if (skeleton.length === 0) {
16
+ throw new Error('Number skeleton cannot be empty')
17
+ }
18
+ // Parse the skeleton
19
+ const stringTokens = skeleton
20
+ .split(WHITE_SPACE_REGEX)
21
+ .filter(x => x.length > 0)
22
+
23
+ const tokens: NumberSkeletonToken[] = []
24
+ for (const stringToken of stringTokens) {
25
+ let stemAndOptions = stringToken.split('/')
26
+ if (stemAndOptions.length === 0) {
27
+ throw new Error('Invalid number skeleton')
28
+ }
29
+
30
+ const [stem, ...options] = stemAndOptions
31
+ for (const option of options) {
32
+ if (option.length === 0) {
33
+ throw new Error('Invalid number skeleton')
34
+ }
35
+ }
36
+
37
+ tokens.push({stem, options})
38
+ }
39
+ return tokens
40
+ }
41
+
42
+ function icuUnitToEcma(unit: string): ExtendedNumberFormatOptions['unit'] {
43
+ return unit.replace(/^(.*?)-/, '') as ExtendedNumberFormatOptions['unit']
44
+ }
45
+
46
+ const FRACTION_PRECISION_REGEX = /^\.(?:(0+)(\*)?|(#+)|(0+)(#+))$/g
47
+ const SIGNIFICANT_PRECISION_REGEX = /^(@+)?(\+|#+)?[rs]?$/g
48
+ const INTEGER_WIDTH_REGEX = /(\*)(0+)|(#+)(0+)|(0+)/g
49
+ const CONCISE_INTEGER_WIDTH_REGEX = /^(0+)$/
50
+
51
+ function parseSignificantPrecision(str: string): ExtendedNumberFormatOptions {
52
+ const result: ExtendedNumberFormatOptions = {}
53
+ if (str[str.length - 1] === 'r') {
54
+ result.roundingPriority = 'morePrecision'
55
+ } else if (str[str.length - 1] === 's') {
56
+ result.roundingPriority = 'lessPrecision'
57
+ }
58
+ str.replace(
59
+ SIGNIFICANT_PRECISION_REGEX,
60
+ function (_: string, g1: string, g2: string | number) {
61
+ // @@@ case
62
+ if (typeof g2 !== 'string') {
63
+ result.minimumSignificantDigits = g1.length
64
+ result.maximumSignificantDigits = g1.length
65
+ }
66
+ // @@@+ case
67
+ else if (g2 === '+') {
68
+ result.minimumSignificantDigits = g1.length
69
+ }
70
+ // .### case
71
+ else if (g1[0] === '#') {
72
+ result.maximumSignificantDigits = g1.length
73
+ }
74
+ // .@@## or .@@@ case
75
+ else {
76
+ result.minimumSignificantDigits = g1.length
77
+ result.maximumSignificantDigits =
78
+ g1.length + (typeof g2 === 'string' ? g2.length : 0)
79
+ }
80
+ return ''
81
+ }
82
+ )
83
+ return result
84
+ }
85
+
86
+ function parseSign(str: string): ExtendedNumberFormatOptions | undefined {
87
+ switch (str) {
88
+ case 'sign-auto':
89
+ return {
90
+ signDisplay: 'auto',
91
+ }
92
+ case 'sign-accounting':
93
+ case '()':
94
+ return {
95
+ currencySign: 'accounting',
96
+ }
97
+ case 'sign-always':
98
+ case '+!':
99
+ return {
100
+ signDisplay: 'always',
101
+ }
102
+ case 'sign-accounting-always':
103
+ case '()!':
104
+ return {
105
+ signDisplay: 'always',
106
+ currencySign: 'accounting',
107
+ }
108
+ case 'sign-except-zero':
109
+ case '+?':
110
+ return {
111
+ signDisplay: 'exceptZero',
112
+ }
113
+ case 'sign-accounting-except-zero':
114
+ case '()?':
115
+ return {
116
+ signDisplay: 'exceptZero',
117
+ currencySign: 'accounting',
118
+ }
119
+ case 'sign-never':
120
+ case '+_':
121
+ return {
122
+ signDisplay: 'never',
123
+ }
124
+ }
125
+ }
126
+
127
+ function parseConciseScientificAndEngineeringStem(
128
+ stem: string
129
+ ): ExtendedNumberFormatOptions | undefined {
130
+ // Engineering
131
+ let result: ExtendedNumberFormatOptions | undefined
132
+ if (stem[0] === 'E' && stem[1] === 'E') {
133
+ result = {
134
+ notation: 'engineering',
135
+ }
136
+ stem = stem.slice(2)
137
+ } else if (stem[0] === 'E') {
138
+ result = {
139
+ notation: 'scientific',
140
+ }
141
+ stem = stem.slice(1)
142
+ }
143
+ if (result) {
144
+ const signDisplay = stem.slice(0, 2)
145
+ if (signDisplay === '+!') {
146
+ result.signDisplay = 'always'
147
+ stem = stem.slice(2)
148
+ } else if (signDisplay === '+?') {
149
+ result.signDisplay = 'exceptZero'
150
+ stem = stem.slice(2)
151
+ }
152
+ if (!CONCISE_INTEGER_WIDTH_REGEX.test(stem)) {
153
+ throw new Error('Malformed concise eng/scientific notation')
154
+ }
155
+ result.minimumIntegerDigits = stem.length
156
+ }
157
+ return result
158
+ }
159
+
160
+ function parseNotationOptions(opt: string): ExtendedNumberFormatOptions {
161
+ const result: ExtendedNumberFormatOptions = {}
162
+ const signOpts = parseSign(opt)
163
+ if (signOpts) {
164
+ return signOpts
165
+ }
166
+ return result
167
+ }
168
+
169
+ /**
170
+ * https://github.com/unicode-org/icu/blob/master/docs/userguide/format_parse/numbers/skeletons.md#skeleton-stems-and-options
171
+ */
172
+ export function parseNumberSkeleton(
173
+ tokens: NumberSkeletonToken[]
174
+ ): ExtendedNumberFormatOptions {
175
+ let result: ExtendedNumberFormatOptions = {}
176
+ for (const token of tokens) {
177
+ switch (token.stem) {
178
+ case 'percent':
179
+ case '%':
180
+ result.style = 'percent'
181
+ continue
182
+ case '%x100':
183
+ result.style = 'percent'
184
+ result.scale = 100
185
+ continue
186
+ case 'currency':
187
+ result.style = 'currency'
188
+ result.currency = token.options[0]
189
+ continue
190
+ case 'group-off':
191
+ case ',_':
192
+ result.useGrouping = false
193
+ continue
194
+ case 'precision-integer':
195
+ case '.':
196
+ result.maximumFractionDigits = 0
197
+ continue
198
+ case 'measure-unit':
199
+ case 'unit':
200
+ result.style = 'unit'
201
+ result.unit = icuUnitToEcma(token.options[0])
202
+ continue
203
+ case 'compact-short':
204
+ case 'K':
205
+ result.notation = 'compact'
206
+ result.compactDisplay = 'short'
207
+ continue
208
+ case 'compact-long':
209
+ case 'KK':
210
+ result.notation = 'compact'
211
+ result.compactDisplay = 'long'
212
+ continue
213
+ case 'scientific':
214
+ result = {
215
+ ...result,
216
+ notation: 'scientific',
217
+ ...token.options.reduce(
218
+ (all, opt) => ({...all, ...parseNotationOptions(opt)}),
219
+ {}
220
+ ),
221
+ }
222
+ continue
223
+ case 'engineering':
224
+ result = {
225
+ ...result,
226
+ notation: 'engineering',
227
+ ...token.options.reduce(
228
+ (all, opt) => ({...all, ...parseNotationOptions(opt)}),
229
+ {}
230
+ ),
231
+ }
232
+ continue
233
+ case 'notation-simple':
234
+ result.notation = 'standard'
235
+ continue
236
+ // https://github.com/unicode-org/icu/blob/master/icu4c/source/i18n/unicode/unumberformatter.h
237
+ case 'unit-width-narrow':
238
+ result.currencyDisplay = 'narrowSymbol'
239
+ result.unitDisplay = 'narrow'
240
+ continue
241
+ case 'unit-width-short':
242
+ result.currencyDisplay = 'code'
243
+ result.unitDisplay = 'short'
244
+ continue
245
+ case 'unit-width-full-name':
246
+ result.currencyDisplay = 'name'
247
+ result.unitDisplay = 'long'
248
+ continue
249
+ case 'unit-width-iso-code':
250
+ result.currencyDisplay = 'symbol'
251
+ continue
252
+ case 'scale':
253
+ result.scale = parseFloat(token.options[0])
254
+ continue
255
+ // https://unicode-org.github.io/icu/userguide/format_parse/numbers/skeletons.html#integer-width
256
+ case 'integer-width':
257
+ if (token.options.length > 1) {
258
+ throw new RangeError(
259
+ 'integer-width stems only accept a single optional option'
260
+ )
261
+ }
262
+ token.options[0].replace(
263
+ INTEGER_WIDTH_REGEX,
264
+ function (
265
+ _: string,
266
+ g1: string,
267
+ g2: string,
268
+ g3: string,
269
+ g4: string,
270
+ g5: string
271
+ ) {
272
+ if (g1) {
273
+ result.minimumIntegerDigits = g2.length
274
+ } else if (g3 && g4) {
275
+ throw new Error(
276
+ 'We currently do not support maximum integer digits'
277
+ )
278
+ } else if (g5) {
279
+ throw new Error(
280
+ 'We currently do not support exact integer digits'
281
+ )
282
+ }
283
+ return ''
284
+ }
285
+ )
286
+ continue
287
+ }
288
+ // https://unicode-org.github.io/icu/userguide/format_parse/numbers/skeletons.html#integer-width
289
+ if (CONCISE_INTEGER_WIDTH_REGEX.test(token.stem)) {
290
+ result.minimumIntegerDigits = token.stem.length
291
+ continue
292
+ }
293
+ if (FRACTION_PRECISION_REGEX.test(token.stem)) {
294
+ // Precision
295
+ // https://unicode-org.github.io/icu/userguide/format_parse/numbers/skeletons.html#fraction-precision
296
+ // precision-integer case
297
+ if (token.options.length > 1) {
298
+ throw new RangeError(
299
+ 'Fraction-precision stems only accept a single optional option'
300
+ )
301
+ }
302
+ token.stem.replace(
303
+ FRACTION_PRECISION_REGEX,
304
+ function (
305
+ _: string,
306
+ g1: string,
307
+ g2: string | number,
308
+ g3: string,
309
+ g4: string,
310
+ g5: string
311
+ ) {
312
+ // .000* case (before ICU67 it was .000+)
313
+ if (g2 === '*') {
314
+ result.minimumFractionDigits = g1.length
315
+ }
316
+ // .### case
317
+ else if (g3 && g3[0] === '#') {
318
+ result.maximumFractionDigits = g3.length
319
+ }
320
+ // .00## case
321
+ else if (g4 && g5) {
322
+ result.minimumFractionDigits = g4.length
323
+ result.maximumFractionDigits = g4.length + g5.length
324
+ } else {
325
+ result.minimumFractionDigits = g1.length
326
+ result.maximumFractionDigits = g1.length
327
+ }
328
+ return ''
329
+ }
330
+ )
331
+
332
+ const opt = token.options[0]
333
+ // https://unicode-org.github.io/icu/userguide/format_parse/numbers/skeletons.html#trailing-zero-display
334
+ if (opt === 'w') {
335
+ result = {...result, trailingZeroDisplay: 'stripIfInteger'}
336
+ } else if (opt) {
337
+ result = {...result, ...parseSignificantPrecision(opt)}
338
+ }
339
+ continue
340
+ }
341
+ // https://unicode-org.github.io/icu/userguide/format_parse/numbers/skeletons.html#significant-digits-precision
342
+ if (SIGNIFICANT_PRECISION_REGEX.test(token.stem)) {
343
+ result = {...result, ...parseSignificantPrecision(token.stem)}
344
+ continue
345
+ }
346
+ const signOpts = parseSign(token.stem)
347
+ if (signOpts) {
348
+ result = {...result, ...signOpts}
349
+ }
350
+ const conciseScientificAndEngineeringOpts =
351
+ parseConciseScientificAndEngineeringStem(token.stem)
352
+ if (conciseScientificAndEngineeringOpts) {
353
+ result = {...result, ...conciseScientificAndEngineeringOpts}
354
+ }
355
+ }
356
+ return result
357
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@formatjs/icu-skeleton-parser",
3
- "version": "1.3.10",
3
+ "version": "1.3.11",
4
4
  "main": "index.js",
5
5
  "module": "lib/index.js",
6
6
  "types": "index.d.ts",
@@ -11,7 +11,7 @@
11
11
  "directory": "packages/icu-skeleton-parser"
12
12
  },
13
13
  "dependencies": {
14
- "@formatjs/ecma402-abstract": "1.11.8",
14
+ "@formatjs/ecma402-abstract": "1.11.9",
15
15
  "tslib": "2.4.0"
16
16
  }
17
- }
17
+ }
@@ -0,0 +1,2 @@
1
+ // @generated from regex-gen.ts
2
+ export const WHITE_SPACE_REGEX = /[\t-\r \x85\u200E\u200F\u2028\u2029]/i
@@ -0,0 +1 @@
1
+ declare module 'regenerate'
@@ -0,0 +1,19 @@
1
+ import './global'
2
+ import regenerate from 'regenerate'
3
+ import {outputFileSync} from 'fs-extra'
4
+ import minimist from 'minimist'
5
+
6
+ function main(args: minimist.ParsedArgs) {
7
+ const set = regenerate().add(
8
+ require('@unicode/unicode-13.0.0/Binary_Property/Pattern_White_Space/code-points.js')
9
+ )
10
+ outputFileSync(
11
+ args.out,
12
+ `// @generated from regex-gen.ts
13
+ export const WHITE_SPACE_REGEX = /${set.toString()}/i`
14
+ )
15
+ }
16
+
17
+ if (require.main === module) {
18
+ main(minimist(process.argv))
19
+ }