@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.
@@ -1,26 +1,34 @@
1
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
- import { ShorthandStyles, parseDateTimeSkeleton } from './SkeletonParser.js';
2
+ * ADOBE CONFIDENTIAL
3
+ * ___________________
4
+ *
5
+ * Copyright 2022 Adobe
6
+ * All Rights Reserved.
7
+ *
8
+ * NOTICE: All information contained herein is, and remains
9
+ * the property of Adobe and its suppliers, if any. The intellectual
10
+ * and technical concepts contained herein are proprietary to Adobe
11
+ * and its suppliers and are protected by all applicable intellectual
12
+ * property laws, including trade secret and copyright laws.
13
+ * Dissemination of this information or reproduction of this material
14
+ * is strictly forbidden unless prior written permission is obtained
15
+ * from Adobe.
16
+ **************************************************************************/
17
+ /**
18
+ * Credit: https://git.corp.adobe.com/dc/dfl/blob/master/src/patterns/dates.js
19
+ */
20
+ // Test Japanese full/half width character support
22
21
 
22
+ import {parseDateTimeSkeleton, ShorthandStyles} from './SkeletonParser.js';
23
+
24
+ // get the localized month names resulting from a given pattern
23
25
  const twelveMonths = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].map(m => new Date(2000, m, 1));
26
+
27
+ /**
28
+ * returns the name of all the months for a given locale and given Date Format Settings
29
+ * @param locale {string}
30
+ * @param options {string} instance of Intl.DateTimeFormatOptions
31
+ */
24
32
  function monthNames(locale, options) {
25
33
  return twelveMonths.map(month => {
26
34
  const parts = new Intl.DateTimeFormat(locale, options).formatToParts(month);
@@ -28,17 +36,32 @@ function monthNames(locale, options) {
28
36
  return m && m.value;
29
37
  });
30
38
  }
39
+
40
+ /**
41
+ * return an array of digits used by a given locale
42
+ * @param locale {string}
43
+ */
31
44
  function digitChars(locale) {
32
45
  return new Intl.NumberFormat(locale, {style:'decimal', useGrouping:false})
33
46
  .format(9876543210)
34
47
  .split('')
35
48
  .reverse();
36
49
  }
50
+
51
+ /**
52
+ * returns the calendar name used in a given locale
53
+ * @param locale {string}
54
+ */
37
55
  function calendarName(locale) {
38
56
  const parts = new Intl.DateTimeFormat(locale, {era:'short'}).formatToParts(new Date());
39
57
  const era = parts.find(p => p.type === 'era')?.value;
40
58
  return era === 'هـ' ? 'islamic' : 'gregory';
41
59
  }
60
+
61
+ /**
62
+ * returns the representation of the time of day for a given language
63
+ * @param language {string}
64
+ */
42
65
  function getDayPeriod(language) {
43
66
  const morning = new Date(2000, 1, 1, 1, 1, 1);
44
67
  const afternoon = new Date(2000, 1, 1, 16, 1, 1);
@@ -51,7 +74,13 @@ function getDayPeriod(language) {
51
74
  fn: (period, obj) => obj.hour += (period === pm.value) ? 12 : 0
52
75
  };
53
76
  }
54
- function offsetMS(dateObj, timeZone) {
77
+
78
+ /**
79
+ * get the offset in MS, given a date and timezone
80
+ * @param dateObj {Date}
81
+ * @param timeZone {string}
82
+ */
83
+ export function offsetMS(dateObj, timeZone) {
55
84
  let tzOffset;
56
85
  try {
57
86
  tzOffset = new Intl.DateTimeFormat('en-US', {timeZone, timeZoneName: 'longOffset'}).format(dateObj);
@@ -60,12 +89,13 @@ function offsetMS(dateObj, timeZone) {
60
89
  }
61
90
  const offset = /GMT([+\-−])?(\d{1,2}):?(\d{0,2})?/.exec(tzOffset);
62
91
  if (!offset) return 0;
63
- const [sign, hours, minutes] = offset.slice(1);
64
- const nHours = isNaN(parseInt(hours)) ? 0 : parseInt(hours);
65
- const nMinutes = isNaN(parseInt(minutes)) ? 0 : parseInt(minutes);
92
+ const [sign, hours, minutes] = offset.slice(1)
93
+ const nHours = isNaN(parseInt(hours)) ? 0 : parseInt(hours)
94
+ const nMinutes = isNaN(parseInt(minutes)) ? 0 : parseInt(minutes)
66
95
  const result = ((nHours * 60) + nMinutes) * 60 * 1000;
67
96
  return sign === '-' ? - result : result;
68
97
  }
98
+
69
99
  function getTimezoneOffsetFrom(otherTimezone) {
70
100
  var date = new Date();
71
101
  function objFromStr(str) {
@@ -80,29 +110,65 @@ function getTimezoneOffsetFrom(otherTimezone) {
80
110
  var other = objFromStr(str);
81
111
  str = date.toLocaleString('en-US', { day: 'numeric', hour: 'numeric', minute: 'numeric', hourCycle: 'h23' });
82
112
  var myLocale = objFromStr(str);
83
- var otherOffset = (other.day * 24 * 60) + (other.hour * 60) + (other.minute);
84
- var myLocaleOffset = (myLocale.day * 24 * 60) + (myLocale.hour * 60) + (myLocale.minute);
113
+ var otherOffset = (other.day * 24 * 60) + (other.hour * 60) + (other.minute); // utc date + otherTimezoneDifference
114
+ var myLocaleOffset = (myLocale.day * 24 * 60) + (myLocale.hour * 60) + (myLocale.minute); // utc date + myTimeZoneDifference
115
+ // (utc date + otherZoneDifference) - (utc date + myZoneDifference) - (-1 * myTimeZoneDifference)
85
116
  return otherOffset - myLocaleOffset - date.getTimezoneOffset();
86
117
  }
87
- function offsetMSFallback(dateObj, timezone) {
88
- const timezoneOffset = getTimezoneOffsetFrom(timezone);
118
+
119
+ export function offsetMSFallback(dateObj, timezone) {
120
+ //const defaultOffset = dateObj.getTimezoneOffset();
121
+ const timezoneOffset = getTimezoneOffsetFrom(timezone)
89
122
  return timezoneOffset * 60 * 1000;
90
123
  }
91
- function adjustTimeZone(dateObj, timeZone) {
124
+
125
+ /**
126
+ * adjust from the default JavaScript timezone to the default timezone
127
+ * @param dateObj {Date}
128
+ * @param timeZone {string}
129
+ */
130
+ export function adjustTimeZone(dateObj, timeZone) {
92
131
  if (dateObj === null) return null;
132
+ // const defaultOffset = new Intl.DateTimeFormat('en-US', { timeZoneName: 'longOffset'}).format(dateObj);
93
133
  let baseDate = dateObj.getTime() - dateObj.getTimezoneOffset() * 60 * 1000;
94
134
  const offset = offsetMS(dateObj, timeZone);
95
- offsetMSFallback(dateObj, timeZone);
135
+ const fallBackOffset = offsetMSFallback(dateObj, timeZone);
96
136
  baseDate += - offset;
137
+
138
+
139
+ // get the offset for the default JS environment
140
+ // return days since the epoch
97
141
  return new Date(baseDate);
98
142
  }
99
- function datetimeToNumber(dateObj) {
143
+
144
+ /**
145
+ * Our script object model treats dates as numbers where the integer portion is days since the epoch,
146
+ * the fractional portion is the number hours in the day
147
+ * @param dateObj {Date}
148
+ * @returns {number}
149
+ */
150
+ export function datetimeToNumber(dateObj) {
100
151
  if (dateObj === null) return 0;
152
+ // return days since the epoch
101
153
  return dateObj.getTime() / ( 1000 * 60 * 60 * 24 );
102
154
  }
103
- function numberToDatetime(num) {
155
+
156
+ /**
157
+ * Our script object model treats dates as numbers where the integer portion is days since the epoch,
158
+ * the fractional portion is the number hours in the day
159
+ * @param num
160
+ * @returns {Date}
161
+ */
162
+ export function numberToDatetime(num) {
104
163
  return new Date(Math.round(num * 1000 * 60 * 60 * 24));
105
164
  }
165
+
166
+ /**
167
+ * in some cases, DateTimeFormat doesn't respect the 'numeric' vs. '2-digit' setting
168
+ * for time values. The function corrects that
169
+ * @param formattedParts instance of Intl.DateTimeFormatPart[]
170
+ * @param parsed
171
+ */
106
172
  function fixDigits(formattedParts, parsed) {
107
173
  ['hour', 'minute', 'second'].forEach(type => {
108
174
  const defn = formattedParts.find(f => f.type === type);
@@ -112,29 +178,55 @@ function fixDigits(formattedParts, parsed) {
112
178
  if (fmt === 'numeric' && defn.value.length === 2 && defn.value.charAt(0) === '0') defn.value = defn.value.slice(1);
113
179
  });
114
180
  }
181
+
115
182
  function fixYear(formattedParts, parsed) {
183
+ // two digit years are handled differently in DateTimeFormat. 00 becomes 1900
184
+ // providing a two digit year 0010 gets formatted to 10 and when parsed becomes 1910
185
+ // Hence we need to pad the year with 0 as required by the skeleton and mentioned in
186
+ // unicode. https://www.unicode.org/reports/tr35/tr35-dates.html#dfst-year
116
187
  const defn = formattedParts.find(f => f.type === 'year');
117
188
  if (!defn) return;
189
+ // eslint-disable-next-line no-unused-vars
118
190
  const chars = parsed.find(pair => pair[0] === 'year')[2];
119
191
  while(defn.value.length < chars) {
120
192
  defn.value = `0${defn.value}`;
121
193
  }
122
194
  }
195
+
196
+ /**
197
+ *
198
+ * @param dateValue {Date}
199
+ * @param language {string}
200
+ * @param skeleton {string}
201
+ * @param timeZone {string}
202
+ * @returns {T}
203
+ */
123
204
  function formatDateToParts(dateValue, language, skeleton, timeZone) {
205
+ // DateTimeFormat renames some of the options in its formatted output
206
+ //@ts-ignore
124
207
  const mappings = key => ({
125
208
  hour12: 'dayPeriod',
126
209
  fractionalSecondDigits: 'fractionalSecond',
127
210
  })[key] || key;
211
+
212
+ // produces an array of name/value pairs of skeleton parts
128
213
  const allParameters = parseDateTimeSkeleton(skeleton, language);
129
214
  allParameters.push(['timeZone', timeZone]);
215
+
130
216
  const parsed = allParameters.filter(p => !p[0].startsWith('x-'));
131
217
  const nonStandard = allParameters.filter(p => p[0].startsWith('x-'));
218
+ // reduce to a set of options that can be used to format
132
219
  const options = Object.fromEntries(parsed);
133
220
  delete options.literal;
221
+
134
222
  const df = new Intl.DateTimeFormat(language, options);
223
+ // formattedParts will have all the pieces we need for our date -- but not in the correct order
135
224
  const formattedParts = df.formatToParts(dateValue);
225
+
136
226
  fixDigits(formattedParts, allParameters);
137
227
  fixYear(formattedParts, parsed);
228
+ // iterate through the original parsed components and use its ordering and literals,
229
+ // and add the formatted pieces
138
230
  return parsed.reduce((result, cur) => {
139
231
  if (cur[0] === 'literal') result.push(cur);
140
232
  else {
@@ -144,25 +236,37 @@ function formatDateToParts(dateValue, language, skeleton, timeZone) {
144
236
  const category = tz[0];
145
237
  if (category === 'Z') {
146
238
  if (tz.length < 4) {
239
+ // handle 'Z', 'ZZ', 'ZZZ' Time Zone: ISO8601 basic hms? / RFC 822
147
240
  v.value = v.value.replace(/(GMT|:)/g, '');
148
241
  if (v.value === '') v.value = '+0000';
149
242
  } else if (tz.length === 5) {
243
+ // 'ZZZZZ' Time Zone: ISO8601 extended hms?
150
244
  if (v.value === 'GMT') v.value = 'Z';
151
245
  else v.value = v.value.replace(/GMT/, '');
152
246
  }
153
247
  }
154
248
  if (category === 'X' || category === 'x') {
155
249
  if (tz.length === 1) {
250
+ // 'X' ISO8601 basic hm?, with Z for 0
251
+ // -08, +0530, Z
252
+ // 'x' ISO8601 basic hm?, without Z for 0
156
253
  v.value = v.value.replace(/(GMT|:(00)?)/g, '');
157
254
  }
158
255
  if (tz.length === 2) {
256
+ // 'XX' ISO8601 basic hm, with Z
257
+ // -0800, Z
258
+ // 'xx' ISO8601 basic hm, without Z
159
259
  v.value = v.value.replace(/(GMT|:)/g, '');
160
260
  }
161
261
  if (tz.length === 3) {
262
+ // 'XXX' ISO8601 extended hm, with Z
263
+ // -08:00, Z
264
+ // 'xxx' ISO8601 extended hm, without Z
162
265
  v.value = v.value.replace(/GMT/g, '');
163
266
  }
164
267
  if (category === 'X' && v.value === '') v.value = 'Z';
165
268
  } else if (tz === 'O') {
269
+ // eliminate 'GMT', leading and trailing zeros
166
270
  v.value = v.value.replace(/GMT/g, '').replace(/0(\d+):/, '$1:').replace(/:00/, '');
167
271
  if (v.value === '') v.value = '+0';
168
272
  }
@@ -172,28 +276,41 @@ function formatDateToParts(dateValue, language, skeleton, timeZone) {
172
276
  return result;
173
277
  }, []);
174
278
  }
175
- function formatDate(dateValue, language, skeleton, timeZone) {
176
- if (skeleton.startsWith('date|')) {
177
- skeleton = skeleton.split('|')[1];
178
- }
279
+
280
+ /**
281
+ *
282
+ * @param dateValue {Date}
283
+ * @param language {string}
284
+ * @param skeleton {string}
285
+ * @param timeZone {string}
286
+ */
287
+ export function formatDate(dateValue, language, skeleton, timeZone) {
179
288
  if (ShorthandStyles.find(type => skeleton.includes(type))) {
180
289
  const options = {timeZone};
290
+ // the skeleton could have two keywords -- one for date, one for time
181
291
  const parts = skeleton.split(/\s/).filter(s => s.length);
182
292
  if (ShorthandStyles.indexOf(parts[0]) > -1) {
183
- options.dateStyle = parts[0];
293
+ options.dateStyle = parts[0]
184
294
  }
185
295
  if (parts.length > 1 && ShorthandStyles.indexOf(parts[1]) > -1) {
186
- options.timeStyle = parts[1];
296
+ options.timeStyle = parts[1]
187
297
  }
188
298
  return new Intl.DateTimeFormat(language, options).format(dateValue);
189
299
  }
190
300
  const parts = formatDateToParts(dateValue, language, skeleton, timeZone);
191
301
  return parts.map(p => p[1]).join('');
192
302
  }
193
- function parseDate(dateString, language, skeleton, timeZone, bUseUTC = false) {
194
- if (skeleton.startsWith('date|')) {
195
- skeleton = skeleton.split('|')[1];
196
- }
303
+
304
+ /**
305
+ *
306
+ * @param dateString {string}
307
+ * @param language {string}
308
+ * @param skeleton {string}
309
+ * @param timeZone {string}
310
+ */
311
+ export function parseDate(dateString, language, skeleton, timeZone, bUseUTC = false) {
312
+ // start by getting all the localized parts of a date/time picture:
313
+ // digits, calendar name
197
314
  const lookups = [];
198
315
  const regexParts = [];
199
316
  const calendar = calendarName(language);
@@ -204,11 +321,13 @@ function parseDate(dateString, language, skeleton, timeZone, bUseUTC = false) {
204
321
  let hourCycle = 'h12';
205
322
  let _bUseUTC = bUseUTC;
206
323
  let _setFullYear = false;
324
+ // functions to process the results of the regex match
207
325
  const isSeparator = str => str.length === 1 && ':-/.'.includes(str);
208
326
  const monthNumber = str => getNumber(str) - 1;
209
327
  const getNumber = str => str.split('').reduce((total, digit) => (total * 10) + digits.indexOf(digit), 0);
210
328
  const yearNumber = templateDigits => str => {
211
329
  let year = getNumber(str);
330
+ //todo: align with AF
212
331
  year = year < 100 && templateDigits === 2 ? year + 2000 : year;
213
332
  if (calendar === 'islamic') year = Math.ceil(year * 0.97 + 622);
214
333
  if (templateDigits > 2 && year < 100) {
@@ -217,51 +336,67 @@ function parseDate(dateString, language, skeleton, timeZone, bUseUTC = false) {
217
336
  return year;
218
337
  };
219
338
  const monthLookup = list => month => list.indexOf(month);
339
+
220
340
  const parsed = parseDateTimeSkeleton(skeleton, language);
221
341
  const months = monthNames(language, Object.fromEntries(parsed));
342
+ // build up a regex expression that identifies each option in the skeleton
343
+ // We build two parallel structures:
344
+ // 1. the regex expression that will extract parts of the date/time
345
+ // 2. a lookup array that will convert the matched results into date/time values
222
346
  parsed.forEach(([option, value, len]) => {
347
+ // use a generic regex pattern for all single-character separator literals.
348
+ // Then we'll be forgiving when it comes to separators: / vs - vs : etc
223
349
  if (option === 'literal') {
224
350
  if (isSeparator(value)) regexParts.push(`[^${digits[0]}-${digits[9]}]`);
225
351
  else regexParts.push(value);
352
+
226
353
  } else if (option === 'month' && ['numeric', '2-digit'].includes(value)) {
227
354
  regexParts.push(twoDigit);
228
355
  lookups.push(['month', monthNumber]);
356
+
229
357
  } else if (option === 'month' && ['formatted', 'long', 'short', 'narrow'].includes(value)) {
230
358
  regexParts.push(`(${months.join('|')})`);
231
359
  lookups.push(['month', monthLookup(months)]);
360
+
232
361
  } else if (['day', 'minute', 'second'].includes(option)) {
233
362
  if (option === 'minute' || option === 'second') {
234
- _bUseUTC = false;
363
+ _bUseUTC = false
235
364
  }
236
365
  regexParts.push(twoDigit);
237
366
  lookups.push([option, getNumber]);
367
+
238
368
  } else if (option === 'fractionalSecondDigits') {
239
- _bUseUTC = false;
369
+ _bUseUTC = false
240
370
  regexParts.push(threeDigit);
241
371
  lookups.push([option, (v, obj) => obj.fractionalSecondDigits + getNumber(v)]);
372
+
242
373
  } else if (option === 'hour') {
243
- _bUseUTC = false;
374
+ _bUseUTC = false
244
375
  regexParts.push(twoDigit);
245
376
  lookups.push([option, (v, obj) => obj.hour + getNumber(v)]);
246
377
  } else if (option === 'year') {
247
378
  regexParts.push('numeric' === value ? fourDigit : twoDigit);
248
379
  lookups.push(['year', yearNumber(len)]);
249
380
  } else if (option === 'dayPeriod') {
250
- _bUseUTC = false;
381
+ _bUseUTC = false
251
382
  const dayPeriod = getDayPeriod(language);
252
383
  if (dayPeriod) {
253
384
  regexParts.push(dayPeriod.regex);
254
385
  lookups.push(['hour', dayPeriod.fn]);
255
386
  }
387
+ // Any other part that we don't need, we'll just add a non-greedy consumption
256
388
  } else if (option === 'hourCycle') {
257
- _bUseUTC = false;
389
+ _bUseUTC = false
258
390
  hourCycle = value;
259
391
  } else if (option === 'x-timeZoneName') {
260
- _bUseUTC = false;
392
+ _bUseUTC = false
393
+ // we handle only the GMT offset picture
261
394
  regexParts.push('(?:GMT|UTC|Z)?([+\\-−0-9]{0,3}:?[0-9]{0,2})');
262
395
  lookups.push([option, (v, obj) => {
263
396
  _bUseUTC = true;
397
+ // v could be undefined if we're on GMT time
264
398
  if (!v) return;
399
+ // replace the unicode minus, then extract hours [and minutes]
265
400
  const timeParts = v.replace(/−/, '-').match(/([+\-\d]{2,3}):?(\d{0,2})/);
266
401
  const hours = timeParts[1] * 1;
267
402
  obj.hour -= hours;
@@ -269,14 +404,17 @@ function parseDate(dateString, language, skeleton, timeZone, bUseUTC = false) {
269
404
  obj.minute -= (hours < 0) ? - mins : mins;
270
405
  }]);
271
406
  } else if (option !== 'timeZoneName') {
272
- _bUseUTC = false;
407
+ _bUseUTC = false
273
408
  regexParts.push('.+?');
274
409
  }
410
+
275
411
  return regexParts;
276
412
  }, []);
277
413
  const regex = new RegExp(regexParts.join(''));
278
414
  const match = dateString.match(regex);
279
415
  if (match === null) return dateString;
416
+
417
+ // now loop through all the matched pieces and build up an object we'll use to create a Date object
280
418
  const dateObj = {year: 1972, month: 0, day: 1, hour: 0, minute: 0, second: 0, fractionalSecondDigits: 0};
281
419
  match.slice(1).forEach((m, index) => {
282
420
  const [element, func] = lookups[index];
@@ -313,8 +451,7 @@ function parseDate(dateString, language, skeleton, timeZone, bUseUTC = false) {
313
451
  }
314
452
  return timeZone == null ? jsDate : adjustTimeZone(jsDate, timeZone);
315
453
  }
316
- function parseDefaultDate(dateString, language, bUseUTC) {
454
+
455
+ export function parseDefaultDate(dateString, language, bUseUTC) {
317
456
  return parseDate(dateString, language, 'short', null, false);
318
457
  }
319
-
320
- export { adjustTimeZone, datetimeToNumber, formatDate, numberToDatetime, offsetMS, offsetMSFallback, parseDate, parseDefaultDate };