@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/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
- case 'a':
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
- case 'O':
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
- if (skeleton.startsWith('date|')) {
366
- skeleton = skeleton.split('|')[1];
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
- /(?:[#]+|[@]+(#+)?|[0]+|[,]|[.]|[-]|[+]|[%]|[¤]{1,4}(?:\/([a-zA-Z]{3}))?|[;]|[K]{1,2}|E{1,2}[+]?|'(?:[^']|'')*')|[^a-zA-Z']+/g;
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, maxSignificantDigits, currencySymbol, offset) => {
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
- const hashes = maxSignificantDigits || "";
591
- order.push(['@', len - hashes.length]);
592
- options.minimumSignificantDigits = len - hashes.length;
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
- if (skel.startsWith('num|')) {
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) {