@aemforms/af-formatters 0.22.68 → 0.22.69
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/esm/afb-formatters.js +763 -0
- package/package.json +5 -3
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
const DATE_TIME_REGEX =
|
|
2
|
+
/(?:[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;
|
|
3
|
+
const ShorthandStyles$1 = ["full", "long", "medium", "short"];
|
|
4
|
+
function getSkeleton(skeleton, language) {
|
|
5
|
+
if (ShorthandStyles$1.find(type => skeleton.includes(type))) {
|
|
6
|
+
const parsed = parseDateStyle(skeleton, language);
|
|
7
|
+
const result = [];
|
|
8
|
+
const symbols = {
|
|
9
|
+
month : 'M',
|
|
10
|
+
year : 'Y',
|
|
11
|
+
day : 'd'
|
|
12
|
+
};
|
|
13
|
+
parsed.forEach(([type, option, length]) => {
|
|
14
|
+
if (type in symbols) {
|
|
15
|
+
result.push(Array(length).fill(symbols[type]).join(''));
|
|
16
|
+
} else if (type === 'literal') {
|
|
17
|
+
result.push(option);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
return result.join('');
|
|
21
|
+
}
|
|
22
|
+
return skeleton;
|
|
23
|
+
}
|
|
24
|
+
function parseDateStyle(skeleton, language) {
|
|
25
|
+
const options = {};
|
|
26
|
+
const styles = skeleton.split(/\s/).filter(s => s.length);
|
|
27
|
+
options.dateStyle = styles[0];
|
|
28
|
+
if (styles.length > 1) options.timeStyle = styles[1];
|
|
29
|
+
const testDate = new Date(2000, 2, 1, 2, 3, 4);
|
|
30
|
+
const parts = new Intl.DateTimeFormat(language, options).formatToParts(testDate);
|
|
31
|
+
const formattedMarch = parts.find(p => p.type === 'month').value;
|
|
32
|
+
const longMarch = new Intl.DateTimeFormat(language, {month: 'long'}).formatToParts(testDate)[0].value;
|
|
33
|
+
const shortMarch = new Intl.DateTimeFormat(language, {month: 'short'}).formatToParts(testDate)[0].value;
|
|
34
|
+
const result = [];
|
|
35
|
+
parts.forEach(({type, value}) => {
|
|
36
|
+
let option;
|
|
37
|
+
if (type === 'month') {
|
|
38
|
+
option = {
|
|
39
|
+
[formattedMarch]: skeleton === 'medium' ? 'short' : 'long',
|
|
40
|
+
[longMarch]: 'long',
|
|
41
|
+
[shortMarch]: 'short',
|
|
42
|
+
'03': '2-digit',
|
|
43
|
+
'3': 'numeric'
|
|
44
|
+
}[value];
|
|
45
|
+
}
|
|
46
|
+
if (type === 'year') option = {'2000': 'numeric', '00': '2-digit'}[value];
|
|
47
|
+
if (['day', 'hour', 'minute', 'second'].includes(type)) option = value.length === 2 ? '2-digit' : 'numeric';
|
|
48
|
+
if (type === 'literal') option = value;
|
|
49
|
+
if (type === 'dayPeriod') option = 'short';
|
|
50
|
+
result.push([type, option, value.length]);
|
|
51
|
+
});
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
function parseDateTimeSkeleton(skeleton, language) {
|
|
55
|
+
if (ShorthandStyles$1.find(type => skeleton.includes(type))) {
|
|
56
|
+
return parseDateStyle(skeleton, language);
|
|
57
|
+
}
|
|
58
|
+
const result = [];
|
|
59
|
+
skeleton.replace(DATE_TIME_REGEX, match => {
|
|
60
|
+
const len = match.length;
|
|
61
|
+
switch (match[0]) {
|
|
62
|
+
case 'G':
|
|
63
|
+
result.push(['era', len === 4 ? 'long' : len === 5 ? 'narrow' : 'short', len]);
|
|
64
|
+
break;
|
|
65
|
+
case 'y':
|
|
66
|
+
result.push(['year', len === 2 ? '2-digit' : 'numeric', len]);
|
|
67
|
+
break;
|
|
68
|
+
case 'Y':
|
|
69
|
+
case 'u':
|
|
70
|
+
case 'U':
|
|
71
|
+
case 'r':
|
|
72
|
+
throw new RangeError(
|
|
73
|
+
'`Y/u/U/r` (year) patterns are not supported, use `y` instead'
|
|
74
|
+
);
|
|
75
|
+
case 'q':
|
|
76
|
+
case 'Q':
|
|
77
|
+
throw new RangeError('`q/Q` (quarter) patterns are not supported');
|
|
78
|
+
case 'M':
|
|
79
|
+
case 'L':
|
|
80
|
+
result.push(['month', ['numeric', '2-digit', 'short', 'long', 'narrow'][len - 1], len]);
|
|
81
|
+
break;
|
|
82
|
+
case 'w':
|
|
83
|
+
case 'W':
|
|
84
|
+
throw new RangeError('`w/W` (week) patterns are not supported');
|
|
85
|
+
case 'd':
|
|
86
|
+
result.push(['day', ['numeric', '2-digit'][len - 1], len]);
|
|
87
|
+
break;
|
|
88
|
+
case 'D':
|
|
89
|
+
case 'F':
|
|
90
|
+
case 'g':
|
|
91
|
+
throw new RangeError(
|
|
92
|
+
'`D/F/g` (day) patterns are not supported, use `d` instead'
|
|
93
|
+
);
|
|
94
|
+
case 'E':
|
|
95
|
+
result.push(['weekday', ['short', 'short', 'short', 'long', 'narrow', 'narrow'][len - 1], len]);
|
|
96
|
+
break;
|
|
97
|
+
case 'e':
|
|
98
|
+
if (len < 4) {
|
|
99
|
+
throw new RangeError('`e..eee` (weekday) patterns are not supported');
|
|
100
|
+
}
|
|
101
|
+
result.push(['weekday', ['short', 'long', 'narrow', 'short'][len - 4], len]);
|
|
102
|
+
break;
|
|
103
|
+
case 'c':
|
|
104
|
+
if (len < 3 || len > 5) {
|
|
105
|
+
throw new RangeError('`c, cc, cccccc` (weekday) patterns are not supported');
|
|
106
|
+
}
|
|
107
|
+
result.push(['weekday', ['short', 'long', 'narrow', 'short'][len - 3], len]);
|
|
108
|
+
break;
|
|
109
|
+
case 'a':
|
|
110
|
+
result.push(['hour12', true, 1]);
|
|
111
|
+
break;
|
|
112
|
+
case 'b':
|
|
113
|
+
case 'B':
|
|
114
|
+
throw new RangeError(
|
|
115
|
+
'`b/B` (period) patterns are not supported, use `a` instead'
|
|
116
|
+
);
|
|
117
|
+
case 'h':
|
|
118
|
+
result.push(['hourCycle', 'h12']);
|
|
119
|
+
result.push(['hour', ['numeric', '2-digit'][len - 1], len]);
|
|
120
|
+
break;
|
|
121
|
+
case 'H':
|
|
122
|
+
result.push(['hourCycle', 'h23', 1]);
|
|
123
|
+
result.push(['hour', ['numeric', '2-digit'][len - 1], len]);
|
|
124
|
+
break;
|
|
125
|
+
case 'K':
|
|
126
|
+
result.push(['hourCycle', 'h11', 1]);
|
|
127
|
+
result.push(['hour', ['numeric', '2-digit'][len - 1], len]);
|
|
128
|
+
break;
|
|
129
|
+
case 'k':
|
|
130
|
+
result.push(['hourCycle', 'h24', 1]);
|
|
131
|
+
result.push(['hour', ['numeric', '2-digit'][len - 1], len]);
|
|
132
|
+
break;
|
|
133
|
+
case 'j':
|
|
134
|
+
case 'J':
|
|
135
|
+
case 'C':
|
|
136
|
+
throw new RangeError(
|
|
137
|
+
'`j/J/C` (hour) patterns are not supported, use `h/H/K/k` instead'
|
|
138
|
+
);
|
|
139
|
+
case 'm':
|
|
140
|
+
result.push(['minute', ['numeric', '2-digit'][len - 1], len]);
|
|
141
|
+
break;
|
|
142
|
+
case 's':
|
|
143
|
+
result.push(['second', ['numeric', '2-digit'][len - 1], len]);
|
|
144
|
+
break;
|
|
145
|
+
case 'S':
|
|
146
|
+
result.push(['fractionalSecondDigits', len, len]);
|
|
147
|
+
break;
|
|
148
|
+
case 'A':
|
|
149
|
+
throw new RangeError(
|
|
150
|
+
'`S/A` (millisecond) patterns are not supported, use `s` instead'
|
|
151
|
+
);
|
|
152
|
+
case 'O':
|
|
153
|
+
result.push(['timeZoneName', len < 4 ? 'shortOffset' : 'longOffset', len]);
|
|
154
|
+
result.push(['x-timeZoneName', len < 4 ? 'O' : 'OOOO', len]);
|
|
155
|
+
break;
|
|
156
|
+
case 'X':
|
|
157
|
+
case 'x':
|
|
158
|
+
case 'Z':
|
|
159
|
+
result.push(['timeZoneName', 'longOffset', 1]);
|
|
160
|
+
result.push(['x-timeZoneName', match, 1]);
|
|
161
|
+
break;
|
|
162
|
+
case 'z':
|
|
163
|
+
case 'v':
|
|
164
|
+
case 'V':
|
|
165
|
+
throw new RangeError(
|
|
166
|
+
'z/v/V` (timeZone) patterns are not supported, use `X/x/Z/O` instead'
|
|
167
|
+
);
|
|
168
|
+
case '\'':
|
|
169
|
+
result.push(['literal', match.slice(1, -1).replace(/''/g, '\''), -1]);
|
|
170
|
+
break;
|
|
171
|
+
default:
|
|
172
|
+
result.push(['literal', match, -1]);
|
|
173
|
+
}
|
|
174
|
+
return '';
|
|
175
|
+
});
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const twelveMonths = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].map(m => new Date(2000, m, 1));
|
|
180
|
+
function monthNames(locale, options) {
|
|
181
|
+
return twelveMonths.map(month => {
|
|
182
|
+
const parts = new Intl.DateTimeFormat(locale, options).formatToParts(month);
|
|
183
|
+
const m = parts.find(p => p.type === 'month');
|
|
184
|
+
return m && m.value;
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
function digitChars(locale) {
|
|
188
|
+
return new Intl.NumberFormat(locale, {style:'decimal', useGrouping:false})
|
|
189
|
+
.format(9876543210)
|
|
190
|
+
.split('')
|
|
191
|
+
.reverse();
|
|
192
|
+
}
|
|
193
|
+
function calendarName(locale) {
|
|
194
|
+
const parts = new Intl.DateTimeFormat(locale, {era:'short'}).formatToParts(new Date());
|
|
195
|
+
const era = parts.find(p => p.type === 'era')?.value;
|
|
196
|
+
return era === 'هـ' ? 'islamic' : 'gregory';
|
|
197
|
+
}
|
|
198
|
+
function getDayPeriod(language) {
|
|
199
|
+
const morning = new Date(2000, 1, 1, 1, 1, 1);
|
|
200
|
+
const afternoon = new Date(2000, 1, 1, 16, 1, 1);
|
|
201
|
+
const df = new Intl.DateTimeFormat(language, {dateStyle: 'full', timeStyle: 'full'});
|
|
202
|
+
const am = df.formatToParts(morning).find(p => p.type === 'dayPeriod');
|
|
203
|
+
const pm = df.formatToParts(afternoon).find(p => p.type === 'dayPeriod');
|
|
204
|
+
if (!am || !pm) return null;
|
|
205
|
+
return {
|
|
206
|
+
regex: `(${am.value}|${pm.value})`,
|
|
207
|
+
fn: (period, obj) => obj.hour += (period === pm.value) ? 12 : 0
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function offsetMS(dateObj, timeZone) {
|
|
211
|
+
let tzOffset;
|
|
212
|
+
try {
|
|
213
|
+
tzOffset = new Intl.DateTimeFormat('en-US', {timeZone, timeZoneName: 'longOffset'}).format(dateObj);
|
|
214
|
+
} catch(e) {
|
|
215
|
+
return offsetMSFallback(dateObj, timeZone);
|
|
216
|
+
}
|
|
217
|
+
const offset = /GMT([+\-−])?(\d{1,2}):?(\d{0,2})?/.exec(tzOffset);
|
|
218
|
+
if (!offset) return 0;
|
|
219
|
+
const [sign, hours, minutes] = offset.slice(1);
|
|
220
|
+
const nHours = isNaN(parseInt(hours)) ? 0 : parseInt(hours);
|
|
221
|
+
const nMinutes = isNaN(parseInt(minutes)) ? 0 : parseInt(minutes);
|
|
222
|
+
const result = ((nHours * 60) + nMinutes) * 60 * 1000;
|
|
223
|
+
return sign === '-' ? - result : result;
|
|
224
|
+
}
|
|
225
|
+
function getTimezoneOffsetFrom(otherTimezone) {
|
|
226
|
+
var date = new Date();
|
|
227
|
+
function objFromStr(str) {
|
|
228
|
+
var array = str.replace(":", " ").split(" ");
|
|
229
|
+
return {
|
|
230
|
+
day: parseInt(array[0]),
|
|
231
|
+
hour: parseInt(array[1]),
|
|
232
|
+
minute: parseInt(array[2])
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
var str = date.toLocaleString('en-US', { timeZone: otherTimezone, day: 'numeric', hour: 'numeric', minute: 'numeric', hourCycle: 'h23' });
|
|
236
|
+
var other = objFromStr(str);
|
|
237
|
+
str = date.toLocaleString('en-US', { day: 'numeric', hour: 'numeric', minute: 'numeric', hourCycle: 'h23' });
|
|
238
|
+
var myLocale = objFromStr(str);
|
|
239
|
+
var otherOffset = (other.day * 24 * 60) + (other.hour * 60) + (other.minute);
|
|
240
|
+
var myLocaleOffset = (myLocale.day * 24 * 60) + (myLocale.hour * 60) + (myLocale.minute);
|
|
241
|
+
return otherOffset - myLocaleOffset - date.getTimezoneOffset();
|
|
242
|
+
}
|
|
243
|
+
function offsetMSFallback(dateObj, timezone) {
|
|
244
|
+
const timezoneOffset = getTimezoneOffsetFrom(timezone);
|
|
245
|
+
return timezoneOffset * 60 * 1000;
|
|
246
|
+
}
|
|
247
|
+
function adjustTimeZone(dateObj, timeZone) {
|
|
248
|
+
if (dateObj === null) return null;
|
|
249
|
+
let baseDate = dateObj.getTime() - dateObj.getTimezoneOffset() * 60 * 1000;
|
|
250
|
+
const offset = offsetMS(dateObj, timeZone);
|
|
251
|
+
offsetMSFallback(dateObj, timeZone);
|
|
252
|
+
baseDate += - offset;
|
|
253
|
+
return new Date(baseDate);
|
|
254
|
+
}
|
|
255
|
+
function datetimeToNumber(dateObj) {
|
|
256
|
+
if (dateObj === null) return 0;
|
|
257
|
+
return dateObj.getTime() / ( 1000 * 60 * 60 * 24 );
|
|
258
|
+
}
|
|
259
|
+
function numberToDatetime(num) {
|
|
260
|
+
return new Date(Math.round(num * 1000 * 60 * 60 * 24));
|
|
261
|
+
}
|
|
262
|
+
function fixDigits(formattedParts, parsed) {
|
|
263
|
+
['hour', 'minute', 'second'].forEach(type => {
|
|
264
|
+
const defn = formattedParts.find(f => f.type === type);
|
|
265
|
+
if (!defn) return;
|
|
266
|
+
const fmt = parsed.find(pair => pair[0] === type)[1];
|
|
267
|
+
if (fmt === '2-digit' && defn.value.length === 1) defn.value = `0${defn.value}`;
|
|
268
|
+
if (fmt === 'numeric' && defn.value.length === 2 && defn.value.charAt(0) === '0') defn.value = defn.value.slice(1);
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
function fixYear(formattedParts, parsed) {
|
|
272
|
+
const defn = formattedParts.find(f => f.type === 'year');
|
|
273
|
+
if (!defn) return;
|
|
274
|
+
const chars = parsed.find(pair => pair[0] === 'year')[2];
|
|
275
|
+
while(defn.value.length < chars) {
|
|
276
|
+
defn.value = `0${defn.value}`;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function formatDateToParts(dateValue, language, skeleton, timeZone) {
|
|
280
|
+
const mappings = key => ({
|
|
281
|
+
hour12: 'dayPeriod',
|
|
282
|
+
fractionalSecondDigits: 'fractionalSecond',
|
|
283
|
+
})[key] || key;
|
|
284
|
+
const allParameters = parseDateTimeSkeleton(skeleton, language);
|
|
285
|
+
allParameters.push(['timeZone', timeZone]);
|
|
286
|
+
const parsed = allParameters.filter(p => !p[0].startsWith('x-'));
|
|
287
|
+
const nonStandard = allParameters.filter(p => p[0].startsWith('x-'));
|
|
288
|
+
const options = Object.fromEntries(parsed);
|
|
289
|
+
delete options.literal;
|
|
290
|
+
const df = new Intl.DateTimeFormat(language, options);
|
|
291
|
+
const formattedParts = df.formatToParts(dateValue);
|
|
292
|
+
fixDigits(formattedParts, allParameters);
|
|
293
|
+
fixYear(formattedParts, parsed);
|
|
294
|
+
return parsed.reduce((result, cur) => {
|
|
295
|
+
if (cur[0] === 'literal') result.push(cur);
|
|
296
|
+
else {
|
|
297
|
+
const v = formattedParts.find(p => p.type === mappings(cur[0]));
|
|
298
|
+
if (v && v.type === 'timeZoneName') {
|
|
299
|
+
const tz = nonStandard.find(p => p[0] === 'x-timeZoneName')[1];
|
|
300
|
+
const category = tz[0];
|
|
301
|
+
if (category === 'Z') {
|
|
302
|
+
if (tz.length < 4) {
|
|
303
|
+
v.value = v.value.replace(/(GMT|:)/g, '');
|
|
304
|
+
if (v.value === '') v.value = '+0000';
|
|
305
|
+
} else if (tz.length === 5) {
|
|
306
|
+
if (v.value === 'GMT') v.value = 'Z';
|
|
307
|
+
else v.value = v.value.replace(/GMT/, '');
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (category === 'X' || category === 'x') {
|
|
311
|
+
if (tz.length === 1) {
|
|
312
|
+
v.value = v.value.replace(/(GMT|:(00)?)/g, '');
|
|
313
|
+
}
|
|
314
|
+
if (tz.length === 2) {
|
|
315
|
+
v.value = v.value.replace(/(GMT|:)/g, '');
|
|
316
|
+
}
|
|
317
|
+
if (tz.length === 3) {
|
|
318
|
+
v.value = v.value.replace(/GMT/g, '');
|
|
319
|
+
}
|
|
320
|
+
if (category === 'X' && v.value === '') v.value = 'Z';
|
|
321
|
+
} else if (tz === 'O') {
|
|
322
|
+
v.value = v.value.replace(/GMT/g, '').replace(/0(\d+):/, '$1:').replace(/:00/, '');
|
|
323
|
+
if (v.value === '') v.value = '+0';
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (v) result.push([v.type, v.value]);
|
|
327
|
+
}
|
|
328
|
+
return result;
|
|
329
|
+
}, []);
|
|
330
|
+
}
|
|
331
|
+
function formatDate(dateValue, language, skeleton, timeZone) {
|
|
332
|
+
if (skeleton.startsWith('date|')) {
|
|
333
|
+
skeleton = skeleton.split('|')[1];
|
|
334
|
+
}
|
|
335
|
+
if (ShorthandStyles$1.find(type => skeleton.includes(type))) {
|
|
336
|
+
const options = {timeZone};
|
|
337
|
+
const parts = skeleton.split(/\s/).filter(s => s.length);
|
|
338
|
+
if (ShorthandStyles$1.indexOf(parts[0]) > -1) {
|
|
339
|
+
options.dateStyle = parts[0];
|
|
340
|
+
}
|
|
341
|
+
if (parts.length > 1 && ShorthandStyles$1.indexOf(parts[1]) > -1) {
|
|
342
|
+
options.timeStyle = parts[1];
|
|
343
|
+
}
|
|
344
|
+
return new Intl.DateTimeFormat(language, options).format(dateValue);
|
|
345
|
+
}
|
|
346
|
+
const parts = formatDateToParts(dateValue, language, skeleton, timeZone);
|
|
347
|
+
return parts.map(p => p[1]).join('');
|
|
348
|
+
}
|
|
349
|
+
function parseDate(dateString, language, skeleton, timeZone, bUseUTC = false) {
|
|
350
|
+
if (skeleton.startsWith('date|')) {
|
|
351
|
+
skeleton = skeleton.split('|')[1];
|
|
352
|
+
}
|
|
353
|
+
const lookups = [];
|
|
354
|
+
const regexParts = [];
|
|
355
|
+
const calendar = calendarName(language);
|
|
356
|
+
const digits = digitChars(language);
|
|
357
|
+
const twoDigit = `([${digits[0]}-${digits[9]}]{1,2})`;
|
|
358
|
+
const threeDigit = `([${digits[0]}-${digits[9]}]{1,3})`;
|
|
359
|
+
const fourDigit = `([${digits[0]}-${digits[9]}]{1,4})`;
|
|
360
|
+
let hourCycle = 'h12';
|
|
361
|
+
let _bUseUTC = bUseUTC;
|
|
362
|
+
let _setFullYear = false;
|
|
363
|
+
const isSeparator = str => str.length === 1 && ':-/.'.includes(str);
|
|
364
|
+
const monthNumber = str => getNumber(str) - 1;
|
|
365
|
+
const getNumber = str => str.split('').reduce((total, digit) => (total * 10) + digits.indexOf(digit), 0);
|
|
366
|
+
const yearNumber = templateDigits => str => {
|
|
367
|
+
let year = getNumber(str);
|
|
368
|
+
year = year < 100 && templateDigits === 2 ? year + 2000 : year;
|
|
369
|
+
if (calendar === 'islamic') year = Math.ceil(year * 0.97 + 622);
|
|
370
|
+
if (templateDigits > 2 && year < 100) {
|
|
371
|
+
_setFullYear = true;
|
|
372
|
+
}
|
|
373
|
+
return year;
|
|
374
|
+
};
|
|
375
|
+
const monthLookup = list => month => list.indexOf(month);
|
|
376
|
+
const parsed = parseDateTimeSkeleton(skeleton, language);
|
|
377
|
+
const months = monthNames(language, Object.fromEntries(parsed));
|
|
378
|
+
parsed.forEach(([option, value, len]) => {
|
|
379
|
+
if (option === 'literal') {
|
|
380
|
+
if (isSeparator(value)) regexParts.push(`[^${digits[0]}-${digits[9]}]`);
|
|
381
|
+
else regexParts.push(value);
|
|
382
|
+
} else if (option === 'month' && ['numeric', '2-digit'].includes(value)) {
|
|
383
|
+
regexParts.push(twoDigit);
|
|
384
|
+
lookups.push(['month', monthNumber]);
|
|
385
|
+
} else if (option === 'month' && ['formatted', 'long', 'short', 'narrow'].includes(value)) {
|
|
386
|
+
regexParts.push(`(${months.join('|')})`);
|
|
387
|
+
lookups.push(['month', monthLookup(months)]);
|
|
388
|
+
} else if (['day', 'minute', 'second'].includes(option)) {
|
|
389
|
+
if (option === 'minute' || option === 'second') {
|
|
390
|
+
_bUseUTC = false;
|
|
391
|
+
}
|
|
392
|
+
regexParts.push(twoDigit);
|
|
393
|
+
lookups.push([option, getNumber]);
|
|
394
|
+
} else if (option === 'fractionalSecondDigits') {
|
|
395
|
+
_bUseUTC = false;
|
|
396
|
+
regexParts.push(threeDigit);
|
|
397
|
+
lookups.push([option, (v, obj) => obj.fractionalSecondDigits + getNumber(v)]);
|
|
398
|
+
} else if (option === 'hour') {
|
|
399
|
+
_bUseUTC = false;
|
|
400
|
+
regexParts.push(twoDigit);
|
|
401
|
+
lookups.push([option, (v, obj) => obj.hour + getNumber(v)]);
|
|
402
|
+
} else if (option === 'year') {
|
|
403
|
+
regexParts.push('numeric' === value ? fourDigit : twoDigit);
|
|
404
|
+
lookups.push(['year', yearNumber(len)]);
|
|
405
|
+
} else if (option === 'dayPeriod') {
|
|
406
|
+
_bUseUTC = false;
|
|
407
|
+
const dayPeriod = getDayPeriod(language);
|
|
408
|
+
if (dayPeriod) {
|
|
409
|
+
regexParts.push(dayPeriod.regex);
|
|
410
|
+
lookups.push(['hour', dayPeriod.fn]);
|
|
411
|
+
}
|
|
412
|
+
} else if (option === 'hourCycle') {
|
|
413
|
+
_bUseUTC = false;
|
|
414
|
+
hourCycle = value;
|
|
415
|
+
} else if (option === 'x-timeZoneName') {
|
|
416
|
+
_bUseUTC = false;
|
|
417
|
+
regexParts.push('(?:GMT|UTC|Z)?([+\\-−0-9]{0,3}:?[0-9]{0,2})');
|
|
418
|
+
lookups.push([option, (v, obj) => {
|
|
419
|
+
_bUseUTC = true;
|
|
420
|
+
if (!v) return;
|
|
421
|
+
const timeParts = v.replace(/−/, '-').match(/([+\-\d]{2,3}):?(\d{0,2})/);
|
|
422
|
+
const hours = timeParts[1] * 1;
|
|
423
|
+
obj.hour -= hours;
|
|
424
|
+
const mins = timeParts.length > 2 ? timeParts[2] * 1 : 0;
|
|
425
|
+
obj.minute -= (hours < 0) ? - mins : mins;
|
|
426
|
+
}]);
|
|
427
|
+
} else if (option !== 'timeZoneName') {
|
|
428
|
+
_bUseUTC = false;
|
|
429
|
+
regexParts.push('.+?');
|
|
430
|
+
}
|
|
431
|
+
return regexParts;
|
|
432
|
+
}, []);
|
|
433
|
+
const regex = new RegExp(regexParts.join(''));
|
|
434
|
+
const match = dateString.match(regex);
|
|
435
|
+
if (match === null) return dateString;
|
|
436
|
+
const dateObj = {year: 1972, month: 0, day: 1, hour: 0, minute: 0, second: 0, fractionalSecondDigits: 0};
|
|
437
|
+
match.slice(1).forEach((m, index) => {
|
|
438
|
+
const [element, func] = lookups[index];
|
|
439
|
+
dateObj[element] = func(m, dateObj);
|
|
440
|
+
});
|
|
441
|
+
if (hourCycle === 'h24' && dateObj.hour === 24) dateObj.hour = 0;
|
|
442
|
+
if (hourCycle === 'h12' && dateObj.hour === 12) dateObj.hour = 0;
|
|
443
|
+
if (_bUseUTC) {
|
|
444
|
+
const utcDate = new Date(Date.UTC(
|
|
445
|
+
dateObj.year,
|
|
446
|
+
dateObj.month,
|
|
447
|
+
dateObj.day,
|
|
448
|
+
dateObj.hour,
|
|
449
|
+
dateObj.minute,
|
|
450
|
+
dateObj.second,
|
|
451
|
+
dateObj.fractionalSecondDigits,
|
|
452
|
+
));
|
|
453
|
+
if (_setFullYear) {
|
|
454
|
+
utcDate.setUTCFullYear(dateObj.year);
|
|
455
|
+
}
|
|
456
|
+
return utcDate;
|
|
457
|
+
}
|
|
458
|
+
const jsDate = new Date(
|
|
459
|
+
dateObj.year,
|
|
460
|
+
dateObj.month,
|
|
461
|
+
dateObj.day,
|
|
462
|
+
dateObj.hour,
|
|
463
|
+
dateObj.minute,
|
|
464
|
+
dateObj.second,
|
|
465
|
+
dateObj.fractionalSecondDigits,
|
|
466
|
+
);
|
|
467
|
+
if (_setFullYear) {
|
|
468
|
+
jsDate.setFullYear(dateObj.year);
|
|
469
|
+
}
|
|
470
|
+
return timeZone == null ? jsDate : adjustTimeZone(jsDate, timeZone);
|
|
471
|
+
}
|
|
472
|
+
function parseDefaultDate(dateString, language, bUseUTC) {
|
|
473
|
+
return parseDate(dateString, language, 'yyyy-MM-dd', null, bUseUTC);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const currencies = {
|
|
477
|
+
'da-DK': 'DKK',
|
|
478
|
+
'de-DE': 'EUR',
|
|
479
|
+
'en-US': 'USD',
|
|
480
|
+
'en-GB': 'GBP',
|
|
481
|
+
'es-ES': 'EUR',
|
|
482
|
+
'fi-FI': 'EUR',
|
|
483
|
+
'fr-FR': 'EUR',
|
|
484
|
+
'it-IT': 'EUR',
|
|
485
|
+
'ja-JP': 'JPY',
|
|
486
|
+
'nb-NO': 'NOK',
|
|
487
|
+
'nl-NL': 'EUR',
|
|
488
|
+
'pt-BR': 'BRL',
|
|
489
|
+
'sv-SE': 'SEK',
|
|
490
|
+
'zh-CN': 'CNY',
|
|
491
|
+
'zh-TW': 'TWD',
|
|
492
|
+
'ko-KR': 'KRW',
|
|
493
|
+
'cs-CZ': 'CZK',
|
|
494
|
+
'pl-PL': 'PLN',
|
|
495
|
+
'ru-RU': 'RUB',
|
|
496
|
+
'tr-TR': 'TRY'
|
|
497
|
+
};
|
|
498
|
+
const locales = Object.keys(currencies);
|
|
499
|
+
const getCurrency = function (locale) {
|
|
500
|
+
if (locales.indexOf(locale) > -1) {
|
|
501
|
+
return currencies[locale]
|
|
502
|
+
} else {
|
|
503
|
+
const matchingLocale = locales.find(x => x.startsWith(locale));
|
|
504
|
+
if (matchingLocale) {
|
|
505
|
+
return currencies[matchingLocale]
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return ''
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
const NUMBER_REGEX =
|
|
512
|
+
/(?:[#]+|[@]+(#+)?|[0]+|[,]|[.]|[-]|[+]|[%]|[¤]{1,4}(?:\/([a-zA-Z]{3}))?|[;]|[K]{1,2}|E{1,2}[+]?|'(?:[^']|'')*')|[^a-zA-Z']+/g;
|
|
513
|
+
const supportedUnits = ['acre', 'bit', 'byte', 'celsius', 'centimeter', 'day',
|
|
514
|
+
'degree', 'fahrenheit', 'fluid-ounce', 'foot', 'gallon', 'gigabit',
|
|
515
|
+
'gigabyte', 'gram', 'hectare', 'hour', 'inch', 'kilobit', 'kilobyte',
|
|
516
|
+
'kilogram', 'kilometer', 'liter', 'megabit', 'megabyte', 'meter', 'mile',
|
|
517
|
+
'mile-scandinavian', 'milliliter', 'millimeter', 'millisecond', 'minute', 'month',
|
|
518
|
+
'ounce', 'percent', 'petabyte', 'pound', 'second', 'stone', 'terabit', 'terabyte', 'week', 'yard', 'year'].join('|');
|
|
519
|
+
const ShorthandStyles = [/^currency(?:\/([a-zA-Z]{3}))?$/, /^decimal$/, /^integer$/, /^percent$/, new RegExp(`^unit\/(${supportedUnits})$`)];
|
|
520
|
+
function parseNumberSkeleton(skeleton, language) {
|
|
521
|
+
const options = {};
|
|
522
|
+
const order = [];
|
|
523
|
+
let match, index;
|
|
524
|
+
for (index = 0; index < ShorthandStyles.length && match == null; index++) {
|
|
525
|
+
match = ShorthandStyles[index].exec(skeleton);
|
|
526
|
+
}
|
|
527
|
+
if (match) {
|
|
528
|
+
switch(index) {
|
|
529
|
+
case 1:
|
|
530
|
+
options.style = 'currency';
|
|
531
|
+
options.currencyDisplay = 'narrowSymbol';
|
|
532
|
+
if (match[1]) {
|
|
533
|
+
options.currency = match[1];
|
|
534
|
+
} else {
|
|
535
|
+
options.currency = getCurrency(language);
|
|
536
|
+
}
|
|
537
|
+
break;
|
|
538
|
+
case 2:
|
|
539
|
+
new Intl.NumberFormat(language, {}).resolvedOptions();
|
|
540
|
+
options.minimumFractionDigits = options.minimumFractionDigits || 2;
|
|
541
|
+
break;
|
|
542
|
+
case 3:
|
|
543
|
+
options.minimumFractionDigits = 0;
|
|
544
|
+
options.maximumFractionDigits = 0;
|
|
545
|
+
break;
|
|
546
|
+
case 4:
|
|
547
|
+
options.style = 'percent';
|
|
548
|
+
options.maximumFractionDigits = 2;
|
|
549
|
+
break;
|
|
550
|
+
case 5:
|
|
551
|
+
options.style = "unit";
|
|
552
|
+
options.unitDisplay = "long";
|
|
553
|
+
options.unit = match[1];
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
return {
|
|
557
|
+
options,
|
|
558
|
+
order
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
options.useGrouping = false;
|
|
562
|
+
options.minimumIntegerDigits = 1;
|
|
563
|
+
options.maximumFractionDigits = 0;
|
|
564
|
+
options.minimumFractionDigits = 0;
|
|
565
|
+
skeleton.replace(NUMBER_REGEX, (match, maxSignificantDigits, currencySymbol, offset) => {
|
|
566
|
+
const len = match.length;
|
|
567
|
+
switch(match[0]) {
|
|
568
|
+
case '#':
|
|
569
|
+
order.push(['digit', len]);
|
|
570
|
+
if (options?.decimal === true) {
|
|
571
|
+
options.maximumFractionDigits = options.minimumFractionDigits + len;
|
|
572
|
+
}
|
|
573
|
+
break;
|
|
574
|
+
case '@':
|
|
575
|
+
if (options?.minimumSignificantDigits) {
|
|
576
|
+
throw "@ symbol should occur together"
|
|
577
|
+
}
|
|
578
|
+
const hashes = maxSignificantDigits || "";
|
|
579
|
+
order.push(['@', len - hashes.length]);
|
|
580
|
+
options.minimumSignificantDigits = len - hashes.length;
|
|
581
|
+
options.maximumSignificantDigits = len;
|
|
582
|
+
order.push(['digit', hashes.length]);
|
|
583
|
+
break;
|
|
584
|
+
case ',':
|
|
585
|
+
if (options?.decimal === true) {
|
|
586
|
+
throw "grouping character not supporting for fractions"
|
|
587
|
+
}
|
|
588
|
+
order.push(['group', 1]);
|
|
589
|
+
options.useGrouping = 'auto';
|
|
590
|
+
break;
|
|
591
|
+
case '.':
|
|
592
|
+
if (options?.decimal) {
|
|
593
|
+
console.error("only one decimal symbol is allowed");
|
|
594
|
+
} else {
|
|
595
|
+
order.push(['decimal', 1]);
|
|
596
|
+
options.decimal = true;
|
|
597
|
+
}
|
|
598
|
+
break;
|
|
599
|
+
case '0':
|
|
600
|
+
order.push('0', len);
|
|
601
|
+
if(options.minimumSignificantDigits || options.maximumSignificantDigits) {
|
|
602
|
+
throw "0 is not supported with @"
|
|
603
|
+
}
|
|
604
|
+
if (options?.decimal === true) {
|
|
605
|
+
options.minimumFractionDigits = len;
|
|
606
|
+
if (!options.maximumFractionDigits) {
|
|
607
|
+
options.maximumFractionDigits = len;
|
|
608
|
+
}
|
|
609
|
+
} else {
|
|
610
|
+
options.minimumIntegerDigits = len;
|
|
611
|
+
}
|
|
612
|
+
break;
|
|
613
|
+
case '-':
|
|
614
|
+
if (offset !== 0) {
|
|
615
|
+
console.error("sign display is always in the beginning");
|
|
616
|
+
}
|
|
617
|
+
options.signDisplay = 'negative';
|
|
618
|
+
order.push(['signDisplay', 1, '-']);
|
|
619
|
+
break;
|
|
620
|
+
case '+':
|
|
621
|
+
if (offset !== 0 && order[order.length - 1][0] === 'E') {
|
|
622
|
+
console.error("sign display is always in the beginning");
|
|
623
|
+
}
|
|
624
|
+
if (offset === 0) {
|
|
625
|
+
options.signDisplay = 'always';
|
|
626
|
+
}
|
|
627
|
+
order.push(['signDisplay', 1, '+']);
|
|
628
|
+
break;
|
|
629
|
+
case '¤':
|
|
630
|
+
if (offset !== 0 && offset !== skeleton.length - 1) {
|
|
631
|
+
console.error("currency display should be either in the beginning or at the end");
|
|
632
|
+
} else {
|
|
633
|
+
options.style = 'currency';
|
|
634
|
+
options.currencyDisplay = ['symbol', 'code', 'name', 'narrowSymbol'][len - 1];
|
|
635
|
+
options.currency = currencySymbol || getCurrency(language);
|
|
636
|
+
order.push(['currency', len]);
|
|
637
|
+
}
|
|
638
|
+
break;
|
|
639
|
+
case '%':
|
|
640
|
+
if (offset !== 0 && offset !== skeleton.length - 1) {
|
|
641
|
+
console.error("percent display should be either in the beginning or at the end");
|
|
642
|
+
} else {
|
|
643
|
+
order.push(['%', 1]);
|
|
644
|
+
options.style = 'percent';
|
|
645
|
+
}
|
|
646
|
+
break;
|
|
647
|
+
case 'E':
|
|
648
|
+
order.push(['E', len]);
|
|
649
|
+
options.style = ['scientific','engineering'](len - 1);
|
|
650
|
+
break;
|
|
651
|
+
default:
|
|
652
|
+
console.error("unknown chars" + match);
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
return {
|
|
656
|
+
options,
|
|
657
|
+
order
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function formatNumber(numberValue, language, skeletn) {
|
|
662
|
+
if (skeletn.startsWith('num|')) {
|
|
663
|
+
skeletn = skel.split('|')[1];
|
|
664
|
+
}
|
|
665
|
+
if (!skeletn) return numberValue
|
|
666
|
+
language = language || "en";
|
|
667
|
+
const {options, order} = parseNumberSkeleton(skeletn, language);
|
|
668
|
+
return new Intl.NumberFormat(language, options).format(numberValue);
|
|
669
|
+
}
|
|
670
|
+
function getMetaInfo(language, skel) {
|
|
671
|
+
const parts = {};
|
|
672
|
+
let options = new Intl.NumberFormat(language, {style:'decimal', useGrouping:false}).formatToParts(9876543210.1);
|
|
673
|
+
parts.digits = options.find(p => p.type === 'integer').value.split('').reverse();
|
|
674
|
+
parts.decimal = options.find(p => p.type === 'decimal').value;
|
|
675
|
+
const gather = type => {
|
|
676
|
+
const find = options.find(p => p.type === type);
|
|
677
|
+
if (find) parts[type] = find.value;
|
|
678
|
+
};
|
|
679
|
+
const parsed = parseNumberSkeleton(skel);
|
|
680
|
+
const nf = new Intl.NumberFormat(language, parsed);
|
|
681
|
+
options = nf.formatToParts(-987654321);
|
|
682
|
+
gather('group');
|
|
683
|
+
gather('minusSign');
|
|
684
|
+
gather('percentSign');
|
|
685
|
+
parts.currency = options.filter(p => p.type === 'currency').map(p => p.value);
|
|
686
|
+
parts.literal = options.filter(p => p.type === 'literal').map(p => p.value);
|
|
687
|
+
options = nf.formatToParts(987654321);
|
|
688
|
+
gather('plusSign');
|
|
689
|
+
gather('exponentSeparator');
|
|
690
|
+
gather('unit');
|
|
691
|
+
return parts;
|
|
692
|
+
}
|
|
693
|
+
function parseNumber(numberString, language, skel) {
|
|
694
|
+
try {
|
|
695
|
+
if (skel.startsWith('num|')) {
|
|
696
|
+
skel = skel.split('|')[1];
|
|
697
|
+
}
|
|
698
|
+
let factor = 1;
|
|
699
|
+
let number = numberString;
|
|
700
|
+
const meta = getMetaInfo(language, skel);
|
|
701
|
+
if (meta.group) number = number.replaceAll(meta.group, '');
|
|
702
|
+
number = number.replace(meta.decimal, '.');
|
|
703
|
+
if (meta.unit) number = number.replaceAll(meta.unit, '');
|
|
704
|
+
if (meta.minusSign && number.includes(meta.minusSign)) {
|
|
705
|
+
number = number.replace(meta.minusSign, '');
|
|
706
|
+
factor *= -1;
|
|
707
|
+
}
|
|
708
|
+
if (meta.percentSign && number.includes(meta.percentSign)) {
|
|
709
|
+
factor = factor/100;
|
|
710
|
+
number = number.replace(meta.percentSign, '');
|
|
711
|
+
}
|
|
712
|
+
meta.currency.forEach(currency => number = number.replace(currency, ''));
|
|
713
|
+
meta.literal.forEach(literal => {
|
|
714
|
+
if (number.includes(literal)) {
|
|
715
|
+
if (literal === '(') factor = factor * -1;
|
|
716
|
+
number = number.replace(literal, '');
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
if (meta.plusSign) number = number.replace(meta.plusSign, '');
|
|
720
|
+
if (meta.exponentSeparator) {
|
|
721
|
+
let e;
|
|
722
|
+
[number, e] = number.split(meta.exponentSeparator);
|
|
723
|
+
factor = factor * Math.pow(10, e);
|
|
724
|
+
}
|
|
725
|
+
const result = factor * number;
|
|
726
|
+
return isNaN(result) ? numberString : result;
|
|
727
|
+
} catch (e) {
|
|
728
|
+
console.dir(e);
|
|
729
|
+
return numberString;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const getCategory = function (skeleton) {
|
|
734
|
+
const chkCategory = skeleton?.match(/^(?:(num|date)\|)?(.+)/);
|
|
735
|
+
return [chkCategory?.[1], chkCategory?.[2]]
|
|
736
|
+
};
|
|
737
|
+
const format = function (value, locale, skeleton, timezone) {
|
|
738
|
+
const [category, skelton] = getCategory(skeleton);
|
|
739
|
+
switch (category) {
|
|
740
|
+
case 'date':
|
|
741
|
+
if (!(value instanceof Date)) {
|
|
742
|
+
value = new Date(value.replace(/-/g, '\/').replace(/T.+/, ''));
|
|
743
|
+
}
|
|
744
|
+
return formatDate(value, locale, skelton, timezone)
|
|
745
|
+
case 'num':
|
|
746
|
+
return formatNumber(value, locale, skelton)
|
|
747
|
+
default:
|
|
748
|
+
throw `unable to deduce the format. The skeleton should be date|<format> for date formats and num|<format> for numbers`
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
const parse = function (value, locale, skeleton, timezone) {
|
|
752
|
+
const [category, skelton] = getCategory(skeleton);
|
|
753
|
+
switch (category) {
|
|
754
|
+
case 'date':
|
|
755
|
+
return parseDate(value, locale, skelton, timezone)
|
|
756
|
+
case 'number':
|
|
757
|
+
return parseNumber(value, locale, skelton)
|
|
758
|
+
default:
|
|
759
|
+
throw `unable to deduce the format. The skeleton should be date|<format> for date formats and num|<format> for numbers`
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
export { datetimeToNumber, format, formatDate, formatNumber, numberToDatetime, parse, parseDate, getSkeleton as parseDateSkeleton, parseDefaultDate, parseNumber };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aemforms/af-formatters",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.69",
|
|
4
4
|
"description": "Formatting Module for Forms Runtime",
|
|
5
5
|
"author": "Adobe Systems",
|
|
6
6
|
"license": "Adobe Proprietary",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"outputDirectory": "./target/test-reports"
|
|
21
21
|
},
|
|
22
22
|
"files": [
|
|
23
|
+
"esm",
|
|
23
24
|
"lib",
|
|
24
25
|
"LICENSE"
|
|
25
26
|
],
|
|
@@ -28,9 +29,10 @@
|
|
|
28
29
|
"eslint": "npx eslint src/**",
|
|
29
30
|
"eslint:fix": "npx eslint --fix src/**",
|
|
30
31
|
"test:ci": "jest --silent --coverage",
|
|
31
|
-
"build": "babel src -d lib",
|
|
32
|
+
"build": "babel src -d lib && npm run build:esm",
|
|
32
33
|
"clean": "rm -rf lib target",
|
|
33
|
-
"prepublishOnly": "npm run build && npm run test"
|
|
34
|
+
"prepublishOnly": "npm run build && npm run test",
|
|
35
|
+
"build:esm": "rollup -c rollup.config.mjs"
|
|
34
36
|
},
|
|
35
37
|
"devDependencies": {
|
|
36
38
|
"@babel/cli": "^7.18.10",
|