@aemforms/af-formatters 0.22.25 → 0.22.26
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/lib/browser/afb-formatters.js +950 -0
- package/lib/cjs/index.cjs +958 -0
- package/lib/esm/date/DateParser.js +457 -0
- package/lib/esm/date/SkeletonParser.js +260 -0
- package/lib/esm/date/index.js +20 -0
- package/lib/esm/index.js +36 -0
- package/lib/esm/number/NumberParser.js +81 -0
- package/lib/esm/number/SkeletonParser.js +172 -0
- package/lib/esm/number/currencies.js +38 -0
- package/package.json +1 -1
|
@@ -0,0 +1,950 @@
|
|
|
1
|
+
/*************************************************************************
|
|
2
|
+
* ADOBE CONFIDENTIAL
|
|
3
|
+
* ___________________
|
|
4
|
+
*
|
|
5
|
+
* Copyright 2022 Adobe
|
|
6
|
+
* All Rights Reserved.
|
|
7
|
+
*
|
|
8
|
+
* NOTICE: All information contained herein is, and remains
|
|
9
|
+
* the property of Adobe and its suppliers, if any. The intellectual
|
|
10
|
+
* and technical concepts contained herein are proprietary to Adobe
|
|
11
|
+
* and its suppliers and are protected by all applicable intellectual
|
|
12
|
+
* property laws, including trade secret and copyright laws.
|
|
13
|
+
* Dissemination of this information or reproduction of this material
|
|
14
|
+
* is strictly forbidden unless prior written permission is obtained
|
|
15
|
+
* from Adobe.
|
|
16
|
+
**************************************************************************/
|
|
17
|
+
/**
|
|
18
|
+
* https://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table
|
|
19
|
+
* Credit: https://git.corp.adobe.com/dc/dfl/blob/master/src/patterns/parseDateTimeSkeleton.js
|
|
20
|
+
* Created a separate library to be used elsewhere as well.
|
|
21
|
+
*/
|
|
22
|
+
const DATE_TIME_REGEX =
|
|
23
|
+
// eslint-disable-next-line max-len
|
|
24
|
+
/(?:[Eec]{1,6}|G{1,5}|[Qq]{1,5}|(?:[yYur]+|U{1,5})|[ML]{1,5}|d{1,2}|D{1,3}|F{1}|[abB]{1,5}|[hkHK]{1,2}|w{1,2}|W{1}|m{1,2}|s{1,2}|[zZOvV]{1,5}|[zZOvVxX]{1,3}|S{1,3}|'(?:[^']|'')*')|[^a-zA-Z']+/g;
|
|
25
|
+
|
|
26
|
+
const ShorthandStyles$1 = ["full", "long", "medium", "short"];
|
|
27
|
+
|
|
28
|
+
function getSkeleton(skeleton, language) {
|
|
29
|
+
if (ShorthandStyles$1.find(type => skeleton.includes(type))) {
|
|
30
|
+
const parsed = parseDateStyle(skeleton, language);
|
|
31
|
+
const result = [];
|
|
32
|
+
const symbols = {
|
|
33
|
+
month : 'M',
|
|
34
|
+
year : 'Y',
|
|
35
|
+
day : 'd'
|
|
36
|
+
};
|
|
37
|
+
parsed.forEach(([type, option, length]) => {
|
|
38
|
+
if (type in symbols) {
|
|
39
|
+
result.push(Array(length).fill(symbols[type]).join(''));
|
|
40
|
+
} else if (type === 'literal') {
|
|
41
|
+
result.push(option);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
return result.join('');
|
|
45
|
+
}
|
|
46
|
+
return skeleton;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
*
|
|
51
|
+
* @param skeleton shorthand style for the date concatenated with shorthand style of time. The
|
|
52
|
+
* Shorthand style for both date and time is one of ['full', 'long', 'medium', 'short'].
|
|
53
|
+
* @param language {string} language to parse the date shorthand style
|
|
54
|
+
* @returns {[*,string][]}
|
|
55
|
+
*/
|
|
56
|
+
function parseDateStyle(skeleton, language) {
|
|
57
|
+
const options = {};
|
|
58
|
+
// the skeleton could have two keywords -- one for date, one for time
|
|
59
|
+
const styles = skeleton.split(/\s/).filter(s => s.length);
|
|
60
|
+
options.dateStyle = styles[0];
|
|
61
|
+
if (styles.length > 1) options.timeStyle = styles[1];
|
|
62
|
+
|
|
63
|
+
const testDate = new Date(2000, 2, 1, 2, 3, 4);
|
|
64
|
+
const parts = new Intl.DateTimeFormat(language, options).formatToParts(testDate);
|
|
65
|
+
// oddly, the formatted month name can be different from the standalone month name
|
|
66
|
+
const formattedMarch = parts.find(p => p.type === 'month').value;
|
|
67
|
+
const longMarch = new Intl.DateTimeFormat(language, {month: 'long'}).formatToParts(testDate)[0].value;
|
|
68
|
+
const shortMarch = new Intl.DateTimeFormat(language, {month: 'short'}).formatToParts(testDate)[0].value;
|
|
69
|
+
const result = [];
|
|
70
|
+
parts.forEach(({type, value}) => {
|
|
71
|
+
let option;
|
|
72
|
+
if (type === 'month') {
|
|
73
|
+
option = {
|
|
74
|
+
[formattedMarch]: skeleton === 'medium' ? 'short' : 'long',
|
|
75
|
+
[longMarch]: 'long',
|
|
76
|
+
[shortMarch]: 'short',
|
|
77
|
+
'03': '2-digit',
|
|
78
|
+
'3': 'numeric'
|
|
79
|
+
}[value];
|
|
80
|
+
}
|
|
81
|
+
if (type === 'year') option = {'2000': 'numeric', '00': '2-digit'}[value];
|
|
82
|
+
if (['day', 'hour', 'minute', 'second'].includes(type)) option = value.length === 2 ? '2-digit' : 'numeric';
|
|
83
|
+
if (type === 'literal') option = value;
|
|
84
|
+
if (type === 'dayPeriod') option = 'short';
|
|
85
|
+
result.push([type, option, value.length]);
|
|
86
|
+
});
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Parse Date time skeleton into Intl.DateTimeFormatOptions parts
|
|
92
|
+
* Ref: https://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table
|
|
93
|
+
*/
|
|
94
|
+
function parseDateTimeSkeleton(skeleton, language) {
|
|
95
|
+
if (ShorthandStyles$1.find(type => skeleton.includes(type))) {
|
|
96
|
+
return parseDateStyle(skeleton, language);
|
|
97
|
+
}
|
|
98
|
+
const result = [];
|
|
99
|
+
skeleton.replace(DATE_TIME_REGEX, match => {
|
|
100
|
+
const len = match.length;
|
|
101
|
+
switch (match[0]) {
|
|
102
|
+
// Era
|
|
103
|
+
case 'G':
|
|
104
|
+
result.push(['era', len === 4 ? 'long' : len === 5 ? 'narrow' : 'short', len]);
|
|
105
|
+
break;
|
|
106
|
+
// Year
|
|
107
|
+
case 'y':
|
|
108
|
+
result.push(['year', len === 2 ? '2-digit' : 'numeric', len]);
|
|
109
|
+
break;
|
|
110
|
+
case 'Y':
|
|
111
|
+
case 'u':
|
|
112
|
+
case 'U':
|
|
113
|
+
case 'r':
|
|
114
|
+
throw new RangeError(
|
|
115
|
+
'`Y/u/U/r` (year) patterns are not supported, use `y` instead'
|
|
116
|
+
);
|
|
117
|
+
// Quarter
|
|
118
|
+
case 'q':
|
|
119
|
+
case 'Q':
|
|
120
|
+
throw new RangeError('`q/Q` (quarter) patterns are not supported');
|
|
121
|
+
// Month
|
|
122
|
+
case 'M':
|
|
123
|
+
case 'L':
|
|
124
|
+
result.push(['month', ['numeric', '2-digit', 'short', 'long', 'narrow'][len - 1], len]);
|
|
125
|
+
break;
|
|
126
|
+
// Week
|
|
127
|
+
case 'w':
|
|
128
|
+
case 'W':
|
|
129
|
+
throw new RangeError('`w/W` (week) patterns are not supported');
|
|
130
|
+
case 'd':
|
|
131
|
+
result.push(['day', ['numeric', '2-digit'][len - 1], len]);
|
|
132
|
+
break;
|
|
133
|
+
case 'D':
|
|
134
|
+
case 'F':
|
|
135
|
+
case 'g':
|
|
136
|
+
throw new RangeError(
|
|
137
|
+
'`D/F/g` (day) patterns are not supported, use `d` instead'
|
|
138
|
+
);
|
|
139
|
+
// Weekday
|
|
140
|
+
case 'E':
|
|
141
|
+
result.push(['weekday', ['short', 'short', 'short', 'long', 'narrow', 'narrow'][len - 1], len]);
|
|
142
|
+
break;
|
|
143
|
+
case 'e':
|
|
144
|
+
if (len < 4) {
|
|
145
|
+
throw new RangeError('`e..eee` (weekday) patterns are not supported');
|
|
146
|
+
}
|
|
147
|
+
result.push(['weekday', ['short', 'long', 'narrow', 'short'][len - 4], len]);
|
|
148
|
+
break;
|
|
149
|
+
case 'c':
|
|
150
|
+
if (len < 3 || len > 5) {
|
|
151
|
+
throw new RangeError('`c, cc, cccccc` (weekday) patterns are not supported');
|
|
152
|
+
}
|
|
153
|
+
result.push(['weekday', ['short', 'long', 'narrow', 'short'][len - 3], len]);
|
|
154
|
+
break;
|
|
155
|
+
// Period
|
|
156
|
+
case 'a': // AM, PM
|
|
157
|
+
result.push(['hour12', true, 1]);
|
|
158
|
+
break;
|
|
159
|
+
case 'b': // am, pm, noon, midnight
|
|
160
|
+
case 'B': // flexible day periods
|
|
161
|
+
throw new RangeError(
|
|
162
|
+
'`b/B` (period) patterns are not supported, use `a` instead'
|
|
163
|
+
);
|
|
164
|
+
// Hour
|
|
165
|
+
case 'h':
|
|
166
|
+
result.push(['hourCycle', 'h12']);
|
|
167
|
+
result.push(['hour', ['numeric', '2-digit'][len - 1], len]);
|
|
168
|
+
break;
|
|
169
|
+
case 'H':
|
|
170
|
+
result.push(['hourCycle', 'h23', 1]);
|
|
171
|
+
result.push(['hour', ['numeric', '2-digit'][len - 1], len]);
|
|
172
|
+
break;
|
|
173
|
+
case 'K':
|
|
174
|
+
result.push(['hourCycle', 'h11', 1]);
|
|
175
|
+
result.push(['hour', ['numeric', '2-digit'][len - 1], len]);
|
|
176
|
+
break;
|
|
177
|
+
case 'k':
|
|
178
|
+
result.push(['hourCycle', 'h24', 1]);
|
|
179
|
+
result.push(['hour', ['numeric', '2-digit'][len - 1], len]);
|
|
180
|
+
break;
|
|
181
|
+
case 'j':
|
|
182
|
+
case 'J':
|
|
183
|
+
case 'C':
|
|
184
|
+
throw new RangeError(
|
|
185
|
+
'`j/J/C` (hour) patterns are not supported, use `h/H/K/k` instead'
|
|
186
|
+
);
|
|
187
|
+
// Minute
|
|
188
|
+
case 'm':
|
|
189
|
+
result.push(['minute', ['numeric', '2-digit'][len - 1], len]);
|
|
190
|
+
break;
|
|
191
|
+
// Second
|
|
192
|
+
case 's':
|
|
193
|
+
result.push(['second', ['numeric', '2-digit'][len - 1], len]);
|
|
194
|
+
break;
|
|
195
|
+
case 'S':
|
|
196
|
+
result.push(['fractionalSecondDigits', len, len]);
|
|
197
|
+
break;
|
|
198
|
+
case 'A':
|
|
199
|
+
throw new RangeError(
|
|
200
|
+
'`S/A` (millisecond) patterns are not supported, use `s` instead'
|
|
201
|
+
);
|
|
202
|
+
// Zone
|
|
203
|
+
case 'O': // timeZone GMT-8 or GMT-08:00
|
|
204
|
+
result.push(['timeZoneName', len < 4 ? 'shortOffset' : 'longOffset', len]);
|
|
205
|
+
result.push(['x-timeZoneName', len < 4 ? 'O' : 'OOOO', len]);
|
|
206
|
+
break;
|
|
207
|
+
case 'X': // 1, 2, 3, 4: The ISO8601 varios formats
|
|
208
|
+
case 'x': // 1, 2, 3, 4: The ISO8601 varios formats
|
|
209
|
+
case 'Z': // 1..3, 4, 5: The ISO8601 varios formats
|
|
210
|
+
// Z, ZZ, ZZZ should produce -0800
|
|
211
|
+
// ZZZZ should produce GMT-08:00
|
|
212
|
+
// ZZZZZ should produce -8:00 or -07:52:58
|
|
213
|
+
result.push(['timeZoneName', 'longOffset', 1]);
|
|
214
|
+
result.push(['x-timeZoneName', match, 1]);
|
|
215
|
+
break;
|
|
216
|
+
case 'z': // 1..3, 4: specific non-location format
|
|
217
|
+
case 'v': // 1, 4: generic non-location format
|
|
218
|
+
case 'V': // 1, 2, 3, 4: time zone ID or city
|
|
219
|
+
throw new RangeError(
|
|
220
|
+
'z/v/V` (timeZone) patterns are not supported, use `X/x/Z/O` instead'
|
|
221
|
+
);
|
|
222
|
+
case '\'':
|
|
223
|
+
result.push(['literal', match.slice(1, -1).replace(/''/g, '\''), -1]);
|
|
224
|
+
break;
|
|
225
|
+
default:
|
|
226
|
+
result.push(['literal', match, -1]);
|
|
227
|
+
}
|
|
228
|
+
return '';
|
|
229
|
+
});
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/*************************************************************************
|
|
234
|
+
* ADOBE CONFIDENTIAL
|
|
235
|
+
* ___________________
|
|
236
|
+
*
|
|
237
|
+
* Copyright 2022 Adobe
|
|
238
|
+
* All Rights Reserved.
|
|
239
|
+
*
|
|
240
|
+
* NOTICE: All information contained herein is, and remains
|
|
241
|
+
* the property of Adobe and its suppliers, if any. The intellectual
|
|
242
|
+
* and technical concepts contained herein are proprietary to Adobe
|
|
243
|
+
* and its suppliers and are protected by all applicable intellectual
|
|
244
|
+
* property laws, including trade secret and copyright laws.
|
|
245
|
+
* Dissemination of this information or reproduction of this material
|
|
246
|
+
* is strictly forbidden unless prior written permission is obtained
|
|
247
|
+
* from Adobe.
|
|
248
|
+
**************************************************************************/
|
|
249
|
+
|
|
250
|
+
// get the localized month names resulting from a given pattern
|
|
251
|
+
const twelveMonths = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].map(m => new Date(2000, m, 1));
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* returns the name of all the months for a given locale and given Date Format Settings
|
|
255
|
+
* @param locale {string}
|
|
256
|
+
* @param options {string} instance of Intl.DateTimeFormatOptions
|
|
257
|
+
*/
|
|
258
|
+
function monthNames(locale, options) {
|
|
259
|
+
return twelveMonths.map(month => {
|
|
260
|
+
const parts = new Intl.DateTimeFormat(locale, options).formatToParts(month);
|
|
261
|
+
const m = parts.find(p => p.type === 'month');
|
|
262
|
+
return m && m.value;
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* return an array of digits used by a given locale
|
|
268
|
+
* @param locale {string}
|
|
269
|
+
*/
|
|
270
|
+
function digitChars(locale) {
|
|
271
|
+
return new Intl.NumberFormat(locale, {style:'decimal', useGrouping:false})
|
|
272
|
+
.format(9876543210)
|
|
273
|
+
.split('')
|
|
274
|
+
.reverse();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* returns the calendar name used in a given locale
|
|
279
|
+
* @param locale {string}
|
|
280
|
+
*/
|
|
281
|
+
function calendarName(locale) {
|
|
282
|
+
const parts = new Intl.DateTimeFormat(locale, {era:'short'}).formatToParts(new Date());
|
|
283
|
+
const era = parts.find(p => p.type === 'era')?.value;
|
|
284
|
+
return era === 'هـ' ? 'islamic' : 'gregory';
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* returns the representation of the time of day for a given language
|
|
289
|
+
* @param language {string}
|
|
290
|
+
*/
|
|
291
|
+
function getDayPeriod(language) {
|
|
292
|
+
const morning = new Date(2000, 1, 1, 1, 1, 1);
|
|
293
|
+
const afternoon = new Date(2000, 1, 1, 16, 1, 1);
|
|
294
|
+
const df = new Intl.DateTimeFormat(language, {dateStyle: 'full', timeStyle: 'full'});
|
|
295
|
+
const am = df.formatToParts(morning).find(p => p.type === 'dayPeriod');
|
|
296
|
+
const pm = df.formatToParts(afternoon).find(p => p.type === 'dayPeriod');
|
|
297
|
+
if (!am || !pm) return null;
|
|
298
|
+
return {
|
|
299
|
+
regex: `(${am.value}|${pm.value})`,
|
|
300
|
+
fn: (period, obj) => obj.hour += (period === pm.value) ? 12 : 0
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* get the offset in MS, given a date and timezone
|
|
306
|
+
* @param dateObj {Date}
|
|
307
|
+
* @param timeZone {string}
|
|
308
|
+
*/
|
|
309
|
+
function offsetMS(dateObj, timeZone) {
|
|
310
|
+
let tzOffset;
|
|
311
|
+
try {
|
|
312
|
+
tzOffset = new Intl.DateTimeFormat('en-US', {timeZone, timeZoneName: 'longOffset'}).format(dateObj);
|
|
313
|
+
} catch(e) {
|
|
314
|
+
return offsetMSFallback(dateObj, timeZone);
|
|
315
|
+
}
|
|
316
|
+
const offset = /GMT([+\-−])?(\d{1,2}):?(\d{0,2})?/.exec(tzOffset);
|
|
317
|
+
if (!offset) return 0;
|
|
318
|
+
const [sign, hours, minutes] = offset.slice(1);
|
|
319
|
+
const nHours = isNaN(parseInt(hours)) ? 0 : parseInt(hours);
|
|
320
|
+
const nMinutes = isNaN(parseInt(minutes)) ? 0 : parseInt(minutes);
|
|
321
|
+
const result = ((nHours * 60) + nMinutes) * 60 * 1000;
|
|
322
|
+
return sign === '-' ? - result : result;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function getTimezoneOffsetFrom(otherTimezone) {
|
|
326
|
+
var date = new Date();
|
|
327
|
+
function objFromStr(str) {
|
|
328
|
+
var array = str.replace(":", " ").split(" ");
|
|
329
|
+
return {
|
|
330
|
+
day: parseInt(array[0]),
|
|
331
|
+
hour: parseInt(array[1]),
|
|
332
|
+
minute: parseInt(array[2])
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
var str = date.toLocaleString('en-US', { timeZone: otherTimezone, day: 'numeric', hour: 'numeric', minute: 'numeric', hourCycle: 'h23' });
|
|
336
|
+
var other = objFromStr(str);
|
|
337
|
+
str = date.toLocaleString('en-US', { day: 'numeric', hour: 'numeric', minute: 'numeric', hourCycle: 'h23' });
|
|
338
|
+
var myLocale = objFromStr(str);
|
|
339
|
+
var otherOffset = (other.day * 24 * 60) + (other.hour * 60) + (other.minute); // utc date + otherTimezoneDifference
|
|
340
|
+
var myLocaleOffset = (myLocale.day * 24 * 60) + (myLocale.hour * 60) + (myLocale.minute); // utc date + myTimeZoneDifference
|
|
341
|
+
// (utc date + otherZoneDifference) - (utc date + myZoneDifference) - (-1 * myTimeZoneDifference)
|
|
342
|
+
return otherOffset - myLocaleOffset - date.getTimezoneOffset();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function offsetMSFallback(dateObj, timezone) {
|
|
346
|
+
//const defaultOffset = dateObj.getTimezoneOffset();
|
|
347
|
+
const timezoneOffset = getTimezoneOffsetFrom(timezone);
|
|
348
|
+
return timezoneOffset * 60 * 1000;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* adjust from the default JavaScript timezone to the default timezone
|
|
353
|
+
* @param dateObj {Date}
|
|
354
|
+
* @param timeZone {string}
|
|
355
|
+
*/
|
|
356
|
+
function adjustTimeZone(dateObj, timeZone) {
|
|
357
|
+
if (dateObj === null) return null;
|
|
358
|
+
// const defaultOffset = new Intl.DateTimeFormat('en-US', { timeZoneName: 'longOffset'}).format(dateObj);
|
|
359
|
+
let baseDate = dateObj.getTime() - dateObj.getTimezoneOffset() * 60 * 1000;
|
|
360
|
+
const offset = offsetMS(dateObj, timeZone);
|
|
361
|
+
offsetMSFallback(dateObj, timeZone);
|
|
362
|
+
baseDate += - offset;
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
// get the offset for the default JS environment
|
|
366
|
+
// return days since the epoch
|
|
367
|
+
return new Date(baseDate);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* in some cases, DateTimeFormat doesn't respect the 'numeric' vs. '2-digit' setting
|
|
372
|
+
* for time values. The function corrects that
|
|
373
|
+
* @param formattedParts instance of Intl.DateTimeFormatPart[]
|
|
374
|
+
* @param parsed
|
|
375
|
+
*/
|
|
376
|
+
function fixDigits(formattedParts, parsed) {
|
|
377
|
+
['hour', 'minute', 'second'].forEach(type => {
|
|
378
|
+
const defn = formattedParts.find(f => f.type === type);
|
|
379
|
+
if (!defn) return;
|
|
380
|
+
const fmt = parsed.find(pair => pair[0] === type)[1];
|
|
381
|
+
if (fmt === '2-digit' && defn.value.length === 1) defn.value = `0${defn.value}`;
|
|
382
|
+
if (fmt === 'numeric' && defn.value.length === 2 && defn.value.charAt(0) === '0') defn.value = defn.value.slice(1);
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function fixYear(formattedParts, parsed) {
|
|
387
|
+
// two digit years are handled differently in DateTimeFormat. 00 becomes 1900
|
|
388
|
+
// providing a two digit year 0010 gets formatted to 10 and when parsed becomes 1910
|
|
389
|
+
// Hence we need to pad the year with 0 as required by the skeleton and mentioned in
|
|
390
|
+
// unicode. https://www.unicode.org/reports/tr35/tr35-dates.html#dfst-year
|
|
391
|
+
const defn = formattedParts.find(f => f.type === 'year');
|
|
392
|
+
if (!defn) return;
|
|
393
|
+
// eslint-disable-next-line no-unused-vars
|
|
394
|
+
const chars = parsed.find(pair => pair[0] === 'year')[2];
|
|
395
|
+
while(defn.value.length < chars) {
|
|
396
|
+
defn.value = `0${defn.value}`;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
*
|
|
402
|
+
* @param dateValue {Date}
|
|
403
|
+
* @param language {string}
|
|
404
|
+
* @param skeleton {string}
|
|
405
|
+
* @param timeZone {string}
|
|
406
|
+
* @returns {T}
|
|
407
|
+
*/
|
|
408
|
+
function formatDateToParts(dateValue, language, skeleton, timeZone) {
|
|
409
|
+
// DateTimeFormat renames some of the options in its formatted output
|
|
410
|
+
//@ts-ignore
|
|
411
|
+
const mappings = key => ({
|
|
412
|
+
hour12: 'dayPeriod',
|
|
413
|
+
fractionalSecondDigits: 'fractionalSecond',
|
|
414
|
+
})[key] || key;
|
|
415
|
+
|
|
416
|
+
// produces an array of name/value pairs of skeleton parts
|
|
417
|
+
const allParameters = parseDateTimeSkeleton(skeleton, language);
|
|
418
|
+
allParameters.push(['timeZone', timeZone]);
|
|
419
|
+
|
|
420
|
+
const parsed = allParameters.filter(p => !p[0].startsWith('x-'));
|
|
421
|
+
const nonStandard = allParameters.filter(p => p[0].startsWith('x-'));
|
|
422
|
+
// reduce to a set of options that can be used to format
|
|
423
|
+
const options = Object.fromEntries(parsed);
|
|
424
|
+
delete options.literal;
|
|
425
|
+
|
|
426
|
+
const df = new Intl.DateTimeFormat(language, options);
|
|
427
|
+
// formattedParts will have all the pieces we need for our date -- but not in the correct order
|
|
428
|
+
const formattedParts = df.formatToParts(dateValue);
|
|
429
|
+
|
|
430
|
+
fixDigits(formattedParts, allParameters);
|
|
431
|
+
fixYear(formattedParts, parsed);
|
|
432
|
+
// iterate through the original parsed components and use its ordering and literals,
|
|
433
|
+
// and add the formatted pieces
|
|
434
|
+
return parsed.reduce((result, cur) => {
|
|
435
|
+
if (cur[0] === 'literal') result.push(cur);
|
|
436
|
+
else {
|
|
437
|
+
const v = formattedParts.find(p => p.type === mappings(cur[0]));
|
|
438
|
+
if (v && v.type === 'timeZoneName') {
|
|
439
|
+
const tz = nonStandard.find(p => p[0] === 'x-timeZoneName')[1];
|
|
440
|
+
const category = tz[0];
|
|
441
|
+
if (category === 'Z') {
|
|
442
|
+
if (tz.length < 4) {
|
|
443
|
+
// handle 'Z', 'ZZ', 'ZZZ' Time Zone: ISO8601 basic hms? / RFC 822
|
|
444
|
+
v.value = v.value.replace(/(GMT|:)/g, '');
|
|
445
|
+
if (v.value === '') v.value = '+0000';
|
|
446
|
+
} else if (tz.length === 5) {
|
|
447
|
+
// 'ZZZZZ' Time Zone: ISO8601 extended hms?
|
|
448
|
+
if (v.value === 'GMT') v.value = 'Z';
|
|
449
|
+
else v.value = v.value.replace(/GMT/, '');
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
if (category === 'X' || category === 'x') {
|
|
453
|
+
if (tz.length === 1) {
|
|
454
|
+
// 'X' ISO8601 basic hm?, with Z for 0
|
|
455
|
+
// -08, +0530, Z
|
|
456
|
+
// 'x' ISO8601 basic hm?, without Z for 0
|
|
457
|
+
v.value = v.value.replace(/(GMT|:(00)?)/g, '');
|
|
458
|
+
}
|
|
459
|
+
if (tz.length === 2) {
|
|
460
|
+
// 'XX' ISO8601 basic hm, with Z
|
|
461
|
+
// -0800, Z
|
|
462
|
+
// 'xx' ISO8601 basic hm, without Z
|
|
463
|
+
v.value = v.value.replace(/(GMT|:)/g, '');
|
|
464
|
+
}
|
|
465
|
+
if (tz.length === 3) {
|
|
466
|
+
// 'XXX' ISO8601 extended hm, with Z
|
|
467
|
+
// -08:00, Z
|
|
468
|
+
// 'xxx' ISO8601 extended hm, without Z
|
|
469
|
+
v.value = v.value.replace(/GMT/g, '');
|
|
470
|
+
}
|
|
471
|
+
if (category === 'X' && v.value === '') v.value = 'Z';
|
|
472
|
+
} else if (tz === 'O') {
|
|
473
|
+
// eliminate 'GMT', leading and trailing zeros
|
|
474
|
+
v.value = v.value.replace(/GMT/g, '').replace(/0(\d+):/, '$1:').replace(/:00/, '');
|
|
475
|
+
if (v.value === '') v.value = '+0';
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (v) result.push([v.type, v.value]);
|
|
479
|
+
}
|
|
480
|
+
return result;
|
|
481
|
+
}, []);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
*
|
|
486
|
+
* @param dateValue {Date}
|
|
487
|
+
* @param language {string}
|
|
488
|
+
* @param skeleton {string}
|
|
489
|
+
* @param timeZone {string}
|
|
490
|
+
*/
|
|
491
|
+
function formatDate(dateValue, language, skeleton, timeZone) {
|
|
492
|
+
if (ShorthandStyles$1.find(type => skeleton.includes(type))) {
|
|
493
|
+
const options = {timeZone};
|
|
494
|
+
// the skeleton could have two keywords -- one for date, one for time
|
|
495
|
+
const parts = skeleton.split(/\s/).filter(s => s.length);
|
|
496
|
+
if (ShorthandStyles$1.indexOf(parts[0]) > -1) {
|
|
497
|
+
options.dateStyle = parts[0];
|
|
498
|
+
}
|
|
499
|
+
if (parts.length > 1 && ShorthandStyles$1.indexOf(parts[1]) > -1) {
|
|
500
|
+
options.timeStyle = parts[1];
|
|
501
|
+
}
|
|
502
|
+
return new Intl.DateTimeFormat(language, options).format(dateValue);
|
|
503
|
+
}
|
|
504
|
+
const parts = formatDateToParts(dateValue, language, skeleton, timeZone);
|
|
505
|
+
return parts.map(p => p[1]).join('');
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
*
|
|
510
|
+
* @param dateString {string}
|
|
511
|
+
* @param language {string}
|
|
512
|
+
* @param skeleton {string}
|
|
513
|
+
* @param timeZone {string}
|
|
514
|
+
*/
|
|
515
|
+
function parseDate(dateString, language, skeleton, timeZone, bUseUTC = false) {
|
|
516
|
+
// start by getting all the localized parts of a date/time picture:
|
|
517
|
+
// digits, calendar name
|
|
518
|
+
const lookups = [];
|
|
519
|
+
const regexParts = [];
|
|
520
|
+
const calendar = calendarName(language);
|
|
521
|
+
const digits = digitChars(language);
|
|
522
|
+
const twoDigit = `([${digits[0]}-${digits[9]}]{1,2})`;
|
|
523
|
+
const threeDigit = `([${digits[0]}-${digits[9]}]{1,3})`;
|
|
524
|
+
const fourDigit = `([${digits[0]}-${digits[9]}]{1,4})`;
|
|
525
|
+
let hourCycle = 'h12';
|
|
526
|
+
let _bUseUTC = bUseUTC;
|
|
527
|
+
let _setFullYear = false;
|
|
528
|
+
// functions to process the results of the regex match
|
|
529
|
+
const isSeparator = str => str.length === 1 && ':-/.'.includes(str);
|
|
530
|
+
const monthNumber = str => getNumber(str) - 1;
|
|
531
|
+
const getNumber = str => str.split('').reduce((total, digit) => (total * 10) + digits.indexOf(digit), 0);
|
|
532
|
+
const yearNumber = templateDigits => str => {
|
|
533
|
+
let year = getNumber(str);
|
|
534
|
+
//todo: align with AF
|
|
535
|
+
year = year < 100 && templateDigits === 2 ? year + 2000 : year;
|
|
536
|
+
if (calendar === 'islamic') year = Math.ceil(year * 0.97 + 622);
|
|
537
|
+
if (templateDigits > 2 && year < 100) {
|
|
538
|
+
_setFullYear = true;
|
|
539
|
+
}
|
|
540
|
+
return year;
|
|
541
|
+
};
|
|
542
|
+
const monthLookup = list => month => list.indexOf(month);
|
|
543
|
+
|
|
544
|
+
const parsed = parseDateTimeSkeleton(skeleton, language);
|
|
545
|
+
const months = monthNames(language, Object.fromEntries(parsed));
|
|
546
|
+
// build up a regex expression that identifies each option in the skeleton
|
|
547
|
+
// We build two parallel structures:
|
|
548
|
+
// 1. the regex expression that will extract parts of the date/time
|
|
549
|
+
// 2. a lookup array that will convert the matched results into date/time values
|
|
550
|
+
parsed.forEach(([option, value, len]) => {
|
|
551
|
+
// use a generic regex pattern for all single-character separator literals.
|
|
552
|
+
// Then we'll be forgiving when it comes to separators: / vs - vs : etc
|
|
553
|
+
if (option === 'literal') {
|
|
554
|
+
if (isSeparator(value)) regexParts.push(`[^${digits[0]}-${digits[9]}]`);
|
|
555
|
+
else regexParts.push(value);
|
|
556
|
+
|
|
557
|
+
} else if (option === 'month' && ['numeric', '2-digit'].includes(value)) {
|
|
558
|
+
regexParts.push(twoDigit);
|
|
559
|
+
lookups.push(['month', monthNumber]);
|
|
560
|
+
|
|
561
|
+
} else if (option === 'month' && ['formatted', 'long', 'short', 'narrow'].includes(value)) {
|
|
562
|
+
regexParts.push(`(${months.join('|')})`);
|
|
563
|
+
lookups.push(['month', monthLookup(months)]);
|
|
564
|
+
|
|
565
|
+
} else if (['day', 'minute', 'second'].includes(option)) {
|
|
566
|
+
if (option === 'minute' || option === 'second') {
|
|
567
|
+
_bUseUTC = false;
|
|
568
|
+
}
|
|
569
|
+
regexParts.push(twoDigit);
|
|
570
|
+
lookups.push([option, getNumber]);
|
|
571
|
+
|
|
572
|
+
} else if (option === 'fractionalSecondDigits') {
|
|
573
|
+
_bUseUTC = false;
|
|
574
|
+
regexParts.push(threeDigit);
|
|
575
|
+
lookups.push([option, (v, obj) => obj.fractionalSecondDigits + getNumber(v)]);
|
|
576
|
+
|
|
577
|
+
} else if (option === 'hour') {
|
|
578
|
+
_bUseUTC = false;
|
|
579
|
+
regexParts.push(twoDigit);
|
|
580
|
+
lookups.push([option, (v, obj) => obj.hour + getNumber(v)]);
|
|
581
|
+
} else if (option === 'year') {
|
|
582
|
+
regexParts.push('numeric' === value ? fourDigit : twoDigit);
|
|
583
|
+
lookups.push(['year', yearNumber(len)]);
|
|
584
|
+
} else if (option === 'dayPeriod') {
|
|
585
|
+
_bUseUTC = false;
|
|
586
|
+
const dayPeriod = getDayPeriod(language);
|
|
587
|
+
if (dayPeriod) {
|
|
588
|
+
regexParts.push(dayPeriod.regex);
|
|
589
|
+
lookups.push(['hour', dayPeriod.fn]);
|
|
590
|
+
}
|
|
591
|
+
// Any other part that we don't need, we'll just add a non-greedy consumption
|
|
592
|
+
} else if (option === 'hourCycle') {
|
|
593
|
+
_bUseUTC = false;
|
|
594
|
+
hourCycle = value;
|
|
595
|
+
} else if (option === 'x-timeZoneName') {
|
|
596
|
+
_bUseUTC = false;
|
|
597
|
+
// we handle only the GMT offset picture
|
|
598
|
+
regexParts.push('(?:GMT|UTC|Z)?([+\\-−0-9]{0,3}:?[0-9]{0,2})');
|
|
599
|
+
lookups.push([option, (v, obj) => {
|
|
600
|
+
_bUseUTC = true;
|
|
601
|
+
// v could be undefined if we're on GMT time
|
|
602
|
+
if (!v) return;
|
|
603
|
+
// replace the unicode minus, then extract hours [and minutes]
|
|
604
|
+
const timeParts = v.replace(/−/, '-').match(/([+\-\d]{2,3}):?(\d{0,2})/);
|
|
605
|
+
const hours = timeParts[1] * 1;
|
|
606
|
+
obj.hour -= hours;
|
|
607
|
+
const mins = timeParts.length > 2 ? timeParts[2] * 1 : 0;
|
|
608
|
+
obj.minute -= (hours < 0) ? - mins : mins;
|
|
609
|
+
}]);
|
|
610
|
+
} else if (option !== 'timeZoneName') {
|
|
611
|
+
_bUseUTC = false;
|
|
612
|
+
regexParts.push('.+?');
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return regexParts;
|
|
616
|
+
}, []);
|
|
617
|
+
const regex = new RegExp(regexParts.join(''));
|
|
618
|
+
const match = dateString.match(regex);
|
|
619
|
+
if (match === null) return dateString;
|
|
620
|
+
|
|
621
|
+
// now loop through all the matched pieces and build up an object we'll use to create a Date object
|
|
622
|
+
const dateObj = {year: 1972, month: 0, day: 1, hour: 0, minute: 0, second: 0, fractionalSecondDigits: 0};
|
|
623
|
+
match.slice(1).forEach((m, index) => {
|
|
624
|
+
const [element, func] = lookups[index];
|
|
625
|
+
dateObj[element] = func(m, dateObj);
|
|
626
|
+
});
|
|
627
|
+
if (hourCycle === 'h24' && dateObj.hour === 24) dateObj.hour = 0;
|
|
628
|
+
if (hourCycle === 'h12' && dateObj.hour === 12) dateObj.hour = 0;
|
|
629
|
+
if (_bUseUTC) {
|
|
630
|
+
const utcDate = new Date(Date.UTC(
|
|
631
|
+
dateObj.year,
|
|
632
|
+
dateObj.month,
|
|
633
|
+
dateObj.day,
|
|
634
|
+
dateObj.hour,
|
|
635
|
+
dateObj.minute,
|
|
636
|
+
dateObj.second,
|
|
637
|
+
dateObj.fractionalSecondDigits,
|
|
638
|
+
));
|
|
639
|
+
if (_setFullYear) {
|
|
640
|
+
utcDate.setUTCFullYear(dateObj.year);
|
|
641
|
+
}
|
|
642
|
+
return utcDate;
|
|
643
|
+
}
|
|
644
|
+
const jsDate = new Date(
|
|
645
|
+
dateObj.year,
|
|
646
|
+
dateObj.month,
|
|
647
|
+
dateObj.day,
|
|
648
|
+
dateObj.hour,
|
|
649
|
+
dateObj.minute,
|
|
650
|
+
dateObj.second,
|
|
651
|
+
dateObj.fractionalSecondDigits,
|
|
652
|
+
);
|
|
653
|
+
if (_setFullYear) {
|
|
654
|
+
jsDate.setFullYear(dateObj.year);
|
|
655
|
+
}
|
|
656
|
+
return timeZone == null ? jsDate : adjustTimeZone(jsDate, timeZone);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const currencies = {
|
|
660
|
+
'da-DK': 'DKK',
|
|
661
|
+
'de-DE': 'EUR',
|
|
662
|
+
'en-US': 'USD',
|
|
663
|
+
'en-GB': 'GBP',
|
|
664
|
+
'es-ES': 'EUR',
|
|
665
|
+
'fi-FI': 'EUR',
|
|
666
|
+
'fr-FR': 'EUR',
|
|
667
|
+
'it-IT': 'EUR',
|
|
668
|
+
'ja-JP': 'JPY',
|
|
669
|
+
'nb-NO': 'NOK',
|
|
670
|
+
'nl-NL': 'EUR',
|
|
671
|
+
'pt-BR': 'BRL',
|
|
672
|
+
'sv-SE': 'SEK',
|
|
673
|
+
'zh-CN': 'CNY',
|
|
674
|
+
'zh-TW': 'TWD',
|
|
675
|
+
'ko-KR': 'KRW',
|
|
676
|
+
'cs-CZ': 'CZK',
|
|
677
|
+
'pl-PL': 'PLN',
|
|
678
|
+
'ru-RU': 'RUB',
|
|
679
|
+
'tr-TR': 'TRY'
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
const locales = Object.keys(currencies);
|
|
683
|
+
|
|
684
|
+
const getCurrency = function (locale) {
|
|
685
|
+
if (locales.indexOf(locale) > -1) {
|
|
686
|
+
return currencies[locale]
|
|
687
|
+
} else {
|
|
688
|
+
const matchingLocale = locales.find(x => x.startsWith(locale));
|
|
689
|
+
if (matchingLocale) {
|
|
690
|
+
return currencies[matchingLocale]
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return ''
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
const NUMBER_REGEX =
|
|
697
|
+
// eslint-disable-next-line max-len
|
|
698
|
+
/(?:[#]+|[@]+(?:#+)?|[0]+|[,]|[.]|[-]|[+]|[%]|[¤]{1,4}(?:\/([a-zA-Z]{3}))?|[;]|[K]{1,2}|E{1,2}[+]?|'(?:[^']|'')*')|[^a-zA-Z']+/g;
|
|
699
|
+
const supportedUnits = ['acre', 'bit', 'byte', 'celsius', 'centimeter', 'day',
|
|
700
|
+
'degree', 'fahrenheit', 'fluid-ounce', 'foot', 'gallon', 'gigabit',
|
|
701
|
+
'gigabyte', 'gram', 'hectare', 'hour', 'inch', 'kilobit', 'kilobyte',
|
|
702
|
+
'kilogram', 'kilometer', 'liter', 'megabit', 'megabyte', 'meter', 'mile',
|
|
703
|
+
'mile-scandinavian', 'milliliter', 'millimeter', 'millisecond', 'minute', 'month',
|
|
704
|
+
'ounce', 'percent', 'petabyte', 'pound', 'second', 'stone', 'terabit', 'terabyte', 'week', 'yard', 'year'].join('|');
|
|
705
|
+
const ShorthandStyles = [/^currency(?:\/([a-zA-Z]{3}))?$/, /^decimal$/, /^integer$/, /^percent$/, new RegExp(`^unit\/(${supportedUnits})$`)];
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
function parseNumberSkeleton(skeleton, language) {
|
|
709
|
+
const options = {};
|
|
710
|
+
const order = [];
|
|
711
|
+
let match, index;
|
|
712
|
+
for (index = 0; index < ShorthandStyles.length && match == null; index++) {
|
|
713
|
+
match = ShorthandStyles[index].exec(skeleton);
|
|
714
|
+
}
|
|
715
|
+
if (match) {
|
|
716
|
+
switch(index) {
|
|
717
|
+
case 1:
|
|
718
|
+
options.style = 'currency';
|
|
719
|
+
options.currencyDisplay = 'narrowSymbol';
|
|
720
|
+
if (match[1]) {
|
|
721
|
+
options.currency = match[1];
|
|
722
|
+
} else {
|
|
723
|
+
options.currency = getCurrency(language);
|
|
724
|
+
}
|
|
725
|
+
break;
|
|
726
|
+
case 2:
|
|
727
|
+
new Intl.NumberFormat(language, {}).resolvedOptions();
|
|
728
|
+
options.minimumFractionDigits = options.minimumFractionDigits || 2;
|
|
729
|
+
break;
|
|
730
|
+
case 3:
|
|
731
|
+
options.minimumFractionDigits = 0;
|
|
732
|
+
options.maximumFractionDigits = 0;
|
|
733
|
+
break;
|
|
734
|
+
case 4:
|
|
735
|
+
options.style = 'percent';
|
|
736
|
+
break;
|
|
737
|
+
case 5:
|
|
738
|
+
options.style = "unit";
|
|
739
|
+
options.unitDisplay = "long";
|
|
740
|
+
options.unit = match[1];
|
|
741
|
+
break;
|
|
742
|
+
}
|
|
743
|
+
return {
|
|
744
|
+
options,
|
|
745
|
+
order
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
options.useGrouping = false;
|
|
749
|
+
options.minimumIntegerDigits = 1;
|
|
750
|
+
options.maximumFractionDigits = 0;
|
|
751
|
+
options.minimumFractionDigits = 0;
|
|
752
|
+
skeleton.replace(NUMBER_REGEX, (match, offset) => {
|
|
753
|
+
const len = match.length;
|
|
754
|
+
switch(match[0]) {
|
|
755
|
+
case '#':
|
|
756
|
+
order.push(['digit', len]);
|
|
757
|
+
if (options?.decimal === true) {
|
|
758
|
+
options.maximumFractionDigits = options.minimumFractionDigits + len;
|
|
759
|
+
}
|
|
760
|
+
break;
|
|
761
|
+
case '@':
|
|
762
|
+
if (options?.minimumSignificantDigits) {
|
|
763
|
+
throw "@ symbol should occur together"
|
|
764
|
+
}
|
|
765
|
+
order.push(['@', len]);
|
|
766
|
+
options.minimumSignificantDigits = len;
|
|
767
|
+
const hashes = match.match(/#+/) || "";
|
|
768
|
+
options.maximumSignificantDigits = len + hashes.length;
|
|
769
|
+
order.push(['digit', hashes.length]);
|
|
770
|
+
break;
|
|
771
|
+
case ',':
|
|
772
|
+
if (options?.decimal === true) {
|
|
773
|
+
throw "grouping character not supporting for fractions"
|
|
774
|
+
}
|
|
775
|
+
order.push(['group', 1]);
|
|
776
|
+
options.useGrouping = 'auto';
|
|
777
|
+
break;
|
|
778
|
+
case '.':
|
|
779
|
+
if (options?.decimal) {
|
|
780
|
+
console.error("only one decimal symbol is allowed");
|
|
781
|
+
} else {
|
|
782
|
+
order.push(['decimal', 1]);
|
|
783
|
+
options.decimal = true;
|
|
784
|
+
}
|
|
785
|
+
break;
|
|
786
|
+
case '0':
|
|
787
|
+
order.push('0', len);
|
|
788
|
+
if(options.minimumSignificantDigits || options.maximumSignificantDigits) {
|
|
789
|
+
throw "0 is not supported with @"
|
|
790
|
+
}
|
|
791
|
+
if (options?.decimal === true) {
|
|
792
|
+
options.minimumFractionDigits = len;
|
|
793
|
+
} else {
|
|
794
|
+
options.minimumIntegerDigits = len;
|
|
795
|
+
}
|
|
796
|
+
break;
|
|
797
|
+
case '-':
|
|
798
|
+
if (offset !== 0) {
|
|
799
|
+
console.error("sign display is always in the beginning");
|
|
800
|
+
}
|
|
801
|
+
options.signDisplay = 'negative';
|
|
802
|
+
order.push(['signDisplay', 1, '-']);
|
|
803
|
+
break;
|
|
804
|
+
case '+':
|
|
805
|
+
if (offset !== 0 && order[order.length - 1][0] === 'E') {
|
|
806
|
+
console.error("sign display is always in the beginning");
|
|
807
|
+
}
|
|
808
|
+
if (offset === 0) {
|
|
809
|
+
options.signDisplay = 'always';
|
|
810
|
+
}
|
|
811
|
+
order.push(['signDisplay', 1, '+']);
|
|
812
|
+
break;
|
|
813
|
+
case '¤':
|
|
814
|
+
if (offset !== 0 && offset !== skeleton.length - 1) {
|
|
815
|
+
console.error("currency display should be either in the beginning or at the end");
|
|
816
|
+
}
|
|
817
|
+
options.style = 'currency';
|
|
818
|
+
options.currencyDisplay = ['symbol', 'code', 'name', 'narrowSymbol'][len -1];
|
|
819
|
+
options.currency = getCurrency(language);
|
|
820
|
+
order.push(['currency', len]);
|
|
821
|
+
break;
|
|
822
|
+
case '%':
|
|
823
|
+
if (offset !== 0 && offset !== skeleton.length - 1) {
|
|
824
|
+
console.error("percent display should be either in the beginning or at the end");
|
|
825
|
+
}
|
|
826
|
+
order.push(['%', 1]);
|
|
827
|
+
options.style = 'percent';
|
|
828
|
+
break;
|
|
829
|
+
case 'E':
|
|
830
|
+
order.push(['E', len]);
|
|
831
|
+
options.style = ['scientific','engineering'](len - 1);
|
|
832
|
+
break;
|
|
833
|
+
default:
|
|
834
|
+
console.error("unknown chars" + match);
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
return {
|
|
838
|
+
options,
|
|
839
|
+
order
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function formatNumber(numberValue, language, skeletn) {
|
|
844
|
+
if (!skeletn) return numberValue
|
|
845
|
+
language = language || "en";
|
|
846
|
+
const {options, order} = parseNumberSkeleton(skeletn, language);
|
|
847
|
+
return new Intl.NumberFormat(language, options).format(numberValue);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function getMetaInfo(language, skel) {
|
|
851
|
+
const parts = {};
|
|
852
|
+
// gather digits and radix symbol
|
|
853
|
+
let options = new Intl.NumberFormat(language, {style:'decimal', useGrouping:false}).formatToParts(9876543210.1);
|
|
854
|
+
parts.digits = options.find(p => p.type === 'integer').value.split('').reverse();
|
|
855
|
+
parts.decimal = options.find(p => p.type === 'decimal').value;
|
|
856
|
+
|
|
857
|
+
// extract type values from the parts
|
|
858
|
+
const gather = type => {
|
|
859
|
+
const find = options.find(p => p.type === type);
|
|
860
|
+
if (find) parts[type] = find.value;
|
|
861
|
+
};
|
|
862
|
+
// now gather the localized parts that correspond to the provided skeleton.
|
|
863
|
+
const parsed = parseNumberSkeleton(skel);
|
|
864
|
+
const nf = new Intl.NumberFormat(language, parsed);
|
|
865
|
+
options = nf.formatToParts(-987654321);
|
|
866
|
+
gather('group');
|
|
867
|
+
gather('minusSign');
|
|
868
|
+
gather('percentSign');
|
|
869
|
+
// it's possible to have multiple currency representations in a single value
|
|
870
|
+
parts.currency = options.filter(p => p.type === 'currency').map(p => p.value);
|
|
871
|
+
// collect all literals. Most likely a literal is an accounting bracket
|
|
872
|
+
parts.literal = options.filter(p => p.type === 'literal').map(p => p.value);
|
|
873
|
+
options = nf.formatToParts(987654321);
|
|
874
|
+
gather('plusSign');
|
|
875
|
+
gather('exponentSeparator');
|
|
876
|
+
gather('unit');
|
|
877
|
+
return parts;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function parseNumber(numberString, language, skel) {
|
|
881
|
+
try {
|
|
882
|
+
// factor will be updated to reflect: negative, percent, exponent etc.
|
|
883
|
+
let factor = 1;
|
|
884
|
+
let number = numberString;
|
|
885
|
+
const meta = getMetaInfo(language, skel);
|
|
886
|
+
if (meta.group) number = number.replaceAll(meta.group, '');
|
|
887
|
+
number = number.replace(meta.decimal, '.');
|
|
888
|
+
if (meta.unit) number = number.replaceAll(meta.unit, '');
|
|
889
|
+
if (meta.minusSign && number.includes(meta.minusSign)) {
|
|
890
|
+
number = number.replace(meta.minusSign, '');
|
|
891
|
+
factor *= -1;
|
|
892
|
+
}
|
|
893
|
+
if (meta.percentSign && number.includes(meta.percentSign)) {
|
|
894
|
+
factor = factor/100;
|
|
895
|
+
number = number.replace(meta.percentSign, '');
|
|
896
|
+
}
|
|
897
|
+
meta.currency.forEach(currency => number = number.replace(currency, ''));
|
|
898
|
+
meta.literal.forEach(literal => {
|
|
899
|
+
if (number.includes(literal)) {
|
|
900
|
+
if (literal === '(') factor = factor * -1;
|
|
901
|
+
number = number.replace(literal, '');
|
|
902
|
+
}
|
|
903
|
+
});
|
|
904
|
+
if (meta.plusSign) number = number.replace(meta.plusSign, '');
|
|
905
|
+
if (meta.exponentSeparator) {
|
|
906
|
+
let e;
|
|
907
|
+
[number, e] = number.split(meta.exponentSeparator);
|
|
908
|
+
factor = factor * Math.pow(10, e);
|
|
909
|
+
}
|
|
910
|
+
const result = factor * number;
|
|
911
|
+
return isNaN(result) ? numberString : result;
|
|
912
|
+
} catch (e) {
|
|
913
|
+
console.dir(e);
|
|
914
|
+
return numberString;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const getCategory = function (skeleton) {
|
|
919
|
+
const chkCategory = skeleton?.match(/^(?:(num|date)\|)?(.+)/);
|
|
920
|
+
return [chkCategory?.[1], chkCategory?.[2]]
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
const format = function (value, locale, skeleton, timezone) {
|
|
924
|
+
const [category, skelton] = getCategory(skeleton);
|
|
925
|
+
switch (category) {
|
|
926
|
+
case 'date':
|
|
927
|
+
if (!(value instanceof Date)) {
|
|
928
|
+
value = new Date(value);
|
|
929
|
+
}
|
|
930
|
+
return formatDate(value, locale, skelton, timezone)
|
|
931
|
+
case 'num':
|
|
932
|
+
return formatNumber(value, locale, skelton)
|
|
933
|
+
default:
|
|
934
|
+
throw `unable to deduce the format. The skeleton should be date|<format> for date formats and num|<format> for numbers`
|
|
935
|
+
}
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
const parse = function (value, locale, skeleton, timezone) {
|
|
939
|
+
const [category, skelton] = getCategory(skeleton);
|
|
940
|
+
switch (category) {
|
|
941
|
+
case 'date':
|
|
942
|
+
return parseDate(value, locale, skelton, timezone)
|
|
943
|
+
case 'number':
|
|
944
|
+
return parseNumber(value, locale, skelton)
|
|
945
|
+
default:
|
|
946
|
+
throw `unable to deduce the format. The skeleton should be date|<format> for date formats and num|<format> for numbers`
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
export { format, formatDate, formatNumber, parse, parseDate, getSkeleton as parseDateSkeleton, parseNumber };
|