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