@aemforms/af-formatters 0.22.25 → 0.22.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,457 @@
1
+ /*************************************************************************
2
+ * ADOBE CONFIDENTIAL
3
+ * ___________________
4
+ *
5
+ * Copyright 2022 Adobe
6
+ * All Rights Reserved.
7
+ *
8
+ * NOTICE: All information contained herein is, and remains
9
+ * the property of Adobe and its suppliers, if any. The intellectual
10
+ * and technical concepts contained herein are proprietary to Adobe
11
+ * and its suppliers and are protected by all applicable intellectual
12
+ * property laws, including trade secret and copyright laws.
13
+ * Dissemination of this information or reproduction of this material
14
+ * is strictly forbidden unless prior written permission is obtained
15
+ * from Adobe.
16
+ **************************************************************************/
17
+ /**
18
+ * Credit: https://git.corp.adobe.com/dc/dfl/blob/master/src/patterns/dates.js
19
+ */
20
+ // Test Japanese full/half width character support
21
+
22
+ import {parseDateTimeSkeleton, ShorthandStyles} from './SkeletonParser.js';
23
+
24
+ // get the localized month names resulting from a given pattern
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
+ */
32
+ function monthNames(locale, options) {
33
+ return twelveMonths.map(month => {
34
+ const parts = new Intl.DateTimeFormat(locale, options).formatToParts(month);
35
+ const m = parts.find(p => p.type === 'month');
36
+ return m && m.value;
37
+ });
38
+ }
39
+
40
+ /**
41
+ * return an array of digits used by a given locale
42
+ * @param locale {string}
43
+ */
44
+ function digitChars(locale) {
45
+ return new Intl.NumberFormat(locale, {style:'decimal', useGrouping:false})
46
+ .format(9876543210)
47
+ .split('')
48
+ .reverse();
49
+ }
50
+
51
+ /**
52
+ * returns the calendar name used in a given locale
53
+ * @param locale {string}
54
+ */
55
+ function calendarName(locale) {
56
+ const parts = new Intl.DateTimeFormat(locale, {era:'short'}).formatToParts(new Date());
57
+ const era = parts.find(p => p.type === 'era')?.value;
58
+ return era === 'هـ' ? 'islamic' : 'gregory';
59
+ }
60
+
61
+ /**
62
+ * returns the representation of the time of day for a given language
63
+ * @param language {string}
64
+ */
65
+ function getDayPeriod(language) {
66
+ const morning = new Date(2000, 1, 1, 1, 1, 1);
67
+ const afternoon = new Date(2000, 1, 1, 16, 1, 1);
68
+ const df = new Intl.DateTimeFormat(language, {dateStyle: 'full', timeStyle: 'full'});
69
+ const am = df.formatToParts(morning).find(p => p.type === 'dayPeriod');
70
+ const pm = df.formatToParts(afternoon).find(p => p.type === 'dayPeriod');
71
+ if (!am || !pm) return null;
72
+ return {
73
+ regex: `(${am.value}|${pm.value})`,
74
+ fn: (period, obj) => obj.hour += (period === pm.value) ? 12 : 0
75
+ };
76
+ }
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) {
84
+ let tzOffset;
85
+ try {
86
+ tzOffset = new Intl.DateTimeFormat('en-US', {timeZone, timeZoneName: 'longOffset'}).format(dateObj);
87
+ } catch(e) {
88
+ return offsetMSFallback(dateObj, timeZone);
89
+ }
90
+ const offset = /GMT([+\-−])?(\d{1,2}):?(\d{0,2})?/.exec(tzOffset);
91
+ if (!offset) return 0;
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)
95
+ const result = ((nHours * 60) + nMinutes) * 60 * 1000;
96
+ return sign === '-' ? - result : result;
97
+ }
98
+
99
+ function getTimezoneOffsetFrom(otherTimezone) {
100
+ var date = new Date();
101
+ function objFromStr(str) {
102
+ var array = str.replace(":", " ").split(" ");
103
+ return {
104
+ day: parseInt(array[0]),
105
+ hour: parseInt(array[1]),
106
+ minute: parseInt(array[2])
107
+ };
108
+ }
109
+ var str = date.toLocaleString('en-US', { timeZone: otherTimezone, day: 'numeric', hour: 'numeric', minute: 'numeric', hourCycle: 'h23' });
110
+ var other = objFromStr(str);
111
+ str = date.toLocaleString('en-US', { day: 'numeric', hour: 'numeric', minute: 'numeric', hourCycle: 'h23' });
112
+ var myLocale = objFromStr(str);
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)
116
+ return otherOffset - myLocaleOffset - date.getTimezoneOffset();
117
+ }
118
+
119
+ export function offsetMSFallback(dateObj, timezone) {
120
+ //const defaultOffset = dateObj.getTimezoneOffset();
121
+ const timezoneOffset = getTimezoneOffsetFrom(timezone)
122
+ return timezoneOffset * 60 * 1000;
123
+ }
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) {
131
+ if (dateObj === null) return null;
132
+ // const defaultOffset = new Intl.DateTimeFormat('en-US', { timeZoneName: 'longOffset'}).format(dateObj);
133
+ let baseDate = dateObj.getTime() - dateObj.getTimezoneOffset() * 60 * 1000;
134
+ const offset = offsetMS(dateObj, timeZone);
135
+ const fallBackOffset = offsetMSFallback(dateObj, timeZone);
136
+ baseDate += - offset;
137
+
138
+
139
+ // get the offset for the default JS environment
140
+ // return days since the epoch
141
+ return new Date(baseDate);
142
+ }
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) {
151
+ if (dateObj === null) return 0;
152
+ // return days since the epoch
153
+ return dateObj.getTime() / ( 1000 * 60 * 60 * 24 );
154
+ }
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) {
163
+ return new Date(Math.round(num * 1000 * 60 * 60 * 24));
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
+ */
172
+ function fixDigits(formattedParts, parsed) {
173
+ ['hour', 'minute', 'second'].forEach(type => {
174
+ const defn = formattedParts.find(f => f.type === type);
175
+ if (!defn) return;
176
+ const fmt = parsed.find(pair => pair[0] === type)[1];
177
+ if (fmt === '2-digit' && defn.value.length === 1) defn.value = `0${defn.value}`;
178
+ if (fmt === 'numeric' && defn.value.length === 2 && defn.value.charAt(0) === '0') defn.value = defn.value.slice(1);
179
+ });
180
+ }
181
+
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
187
+ const defn = formattedParts.find(f => f.type === 'year');
188
+ if (!defn) return;
189
+ // eslint-disable-next-line no-unused-vars
190
+ const chars = parsed.find(pair => pair[0] === 'year')[2];
191
+ while(defn.value.length < chars) {
192
+ defn.value = `0${defn.value}`;
193
+ }
194
+ }
195
+
196
+ /**
197
+ *
198
+ * @param dateValue {Date}
199
+ * @param language {string}
200
+ * @param skeleton {string}
201
+ * @param timeZone {string}
202
+ * @returns {T}
203
+ */
204
+ function formatDateToParts(dateValue, language, skeleton, timeZone) {
205
+ // DateTimeFormat renames some of the options in its formatted output
206
+ //@ts-ignore
207
+ const mappings = key => ({
208
+ hour12: 'dayPeriod',
209
+ fractionalSecondDigits: 'fractionalSecond',
210
+ })[key] || key;
211
+
212
+ // produces an array of name/value pairs of skeleton parts
213
+ const allParameters = parseDateTimeSkeleton(skeleton, language);
214
+ allParameters.push(['timeZone', timeZone]);
215
+
216
+ const parsed = allParameters.filter(p => !p[0].startsWith('x-'));
217
+ const nonStandard = allParameters.filter(p => p[0].startsWith('x-'));
218
+ // reduce to a set of options that can be used to format
219
+ const options = Object.fromEntries(parsed);
220
+ delete options.literal;
221
+
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
224
+ const formattedParts = df.formatToParts(dateValue);
225
+
226
+ fixDigits(formattedParts, allParameters);
227
+ fixYear(formattedParts, parsed);
228
+ // iterate through the original parsed components and use its ordering and literals,
229
+ // and add the formatted pieces
230
+ return parsed.reduce((result, cur) => {
231
+ if (cur[0] === 'literal') result.push(cur);
232
+ else {
233
+ const v = formattedParts.find(p => p.type === mappings(cur[0]));
234
+ if (v && v.type === 'timeZoneName') {
235
+ const tz = nonStandard.find(p => p[0] === 'x-timeZoneName')[1];
236
+ const category = tz[0];
237
+ if (category === 'Z') {
238
+ if (tz.length < 4) {
239
+ // handle 'Z', 'ZZ', 'ZZZ' Time Zone: ISO8601 basic hms? / RFC 822
240
+ v.value = v.value.replace(/(GMT|:)/g, '');
241
+ if (v.value === '') v.value = '+0000';
242
+ } else if (tz.length === 5) {
243
+ // 'ZZZZZ' Time Zone: ISO8601 extended hms?
244
+ if (v.value === 'GMT') v.value = 'Z';
245
+ else v.value = v.value.replace(/GMT/, '');
246
+ }
247
+ }
248
+ if (category === 'X' || category === 'x') {
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
253
+ v.value = v.value.replace(/(GMT|:(00)?)/g, '');
254
+ }
255
+ if (tz.length === 2) {
256
+ // 'XX' ISO8601 basic hm, with Z
257
+ // -0800, Z
258
+ // 'xx' ISO8601 basic hm, without Z
259
+ v.value = v.value.replace(/(GMT|:)/g, '');
260
+ }
261
+ if (tz.length === 3) {
262
+ // 'XXX' ISO8601 extended hm, with Z
263
+ // -08:00, Z
264
+ // 'xxx' ISO8601 extended hm, without Z
265
+ v.value = v.value.replace(/GMT/g, '');
266
+ }
267
+ if (category === 'X' && v.value === '') v.value = 'Z';
268
+ } else if (tz === 'O') {
269
+ // eliminate 'GMT', leading and trailing zeros
270
+ v.value = v.value.replace(/GMT/g, '').replace(/0(\d+):/, '$1:').replace(/:00/, '');
271
+ if (v.value === '') v.value = '+0';
272
+ }
273
+ }
274
+ if (v) result.push([v.type, v.value]);
275
+ }
276
+ return result;
277
+ }, []);
278
+ }
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) {
288
+ if (ShorthandStyles.find(type => skeleton.includes(type))) {
289
+ const options = {timeZone};
290
+ // the skeleton could have two keywords -- one for date, one for time
291
+ const parts = skeleton.split(/\s/).filter(s => s.length);
292
+ if (ShorthandStyles.indexOf(parts[0]) > -1) {
293
+ options.dateStyle = parts[0]
294
+ }
295
+ if (parts.length > 1 && ShorthandStyles.indexOf(parts[1]) > -1) {
296
+ options.timeStyle = parts[1]
297
+ }
298
+ return new Intl.DateTimeFormat(language, options).format(dateValue);
299
+ }
300
+ const parts = formatDateToParts(dateValue, language, skeleton, timeZone);
301
+ return parts.map(p => p[1]).join('');
302
+ }
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
314
+ const lookups = [];
315
+ const regexParts = [];
316
+ const calendar = calendarName(language);
317
+ const digits = digitChars(language);
318
+ const twoDigit = `([${digits[0]}-${digits[9]}]{1,2})`;
319
+ const threeDigit = `([${digits[0]}-${digits[9]}]{1,3})`;
320
+ const fourDigit = `([${digits[0]}-${digits[9]}]{1,4})`;
321
+ let hourCycle = 'h12';
322
+ let _bUseUTC = bUseUTC;
323
+ let _setFullYear = false;
324
+ // functions to process the results of the regex match
325
+ const isSeparator = str => str.length === 1 && ':-/.'.includes(str);
326
+ const monthNumber = str => getNumber(str) - 1;
327
+ const getNumber = str => str.split('').reduce((total, digit) => (total * 10) + digits.indexOf(digit), 0);
328
+ const yearNumber = templateDigits => str => {
329
+ let year = getNumber(str);
330
+ //todo: align with AF
331
+ year = year < 100 && templateDigits === 2 ? year + 2000 : year;
332
+ if (calendar === 'islamic') year = Math.ceil(year * 0.97 + 622);
333
+ if (templateDigits > 2 && year < 100) {
334
+ _setFullYear = true;
335
+ }
336
+ return year;
337
+ };
338
+ const monthLookup = list => month => list.indexOf(month);
339
+
340
+ const parsed = parseDateTimeSkeleton(skeleton, language);
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
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
349
+ if (option === 'literal') {
350
+ if (isSeparator(value)) regexParts.push(`[^${digits[0]}-${digits[9]}]`);
351
+ else regexParts.push(value);
352
+
353
+ } else if (option === 'month' && ['numeric', '2-digit'].includes(value)) {
354
+ regexParts.push(twoDigit);
355
+ lookups.push(['month', monthNumber]);
356
+
357
+ } else if (option === 'month' && ['formatted', 'long', 'short', 'narrow'].includes(value)) {
358
+ regexParts.push(`(${months.join('|')})`);
359
+ lookups.push(['month', monthLookup(months)]);
360
+
361
+ } else if (['day', 'minute', 'second'].includes(option)) {
362
+ if (option === 'minute' || option === 'second') {
363
+ _bUseUTC = false
364
+ }
365
+ regexParts.push(twoDigit);
366
+ lookups.push([option, getNumber]);
367
+
368
+ } else if (option === 'fractionalSecondDigits') {
369
+ _bUseUTC = false
370
+ regexParts.push(threeDigit);
371
+ lookups.push([option, (v, obj) => obj.fractionalSecondDigits + getNumber(v)]);
372
+
373
+ } else if (option === 'hour') {
374
+ _bUseUTC = false
375
+ regexParts.push(twoDigit);
376
+ lookups.push([option, (v, obj) => obj.hour + getNumber(v)]);
377
+ } else if (option === 'year') {
378
+ regexParts.push('numeric' === value ? fourDigit : twoDigit);
379
+ lookups.push(['year', yearNumber(len)]);
380
+ } else if (option === 'dayPeriod') {
381
+ _bUseUTC = false
382
+ const dayPeriod = getDayPeriod(language);
383
+ if (dayPeriod) {
384
+ regexParts.push(dayPeriod.regex);
385
+ lookups.push(['hour', dayPeriod.fn]);
386
+ }
387
+ // Any other part that we don't need, we'll just add a non-greedy consumption
388
+ } else if (option === 'hourCycle') {
389
+ _bUseUTC = false
390
+ hourCycle = value;
391
+ } else if (option === 'x-timeZoneName') {
392
+ _bUseUTC = false
393
+ // we handle only the GMT offset picture
394
+ regexParts.push('(?:GMT|UTC|Z)?([+\\-−0-9]{0,3}:?[0-9]{0,2})');
395
+ lookups.push([option, (v, obj) => {
396
+ _bUseUTC = true;
397
+ // v could be undefined if we're on GMT time
398
+ if (!v) return;
399
+ // replace the unicode minus, then extract hours [and minutes]
400
+ const timeParts = v.replace(/−/, '-').match(/([+\-\d]{2,3}):?(\d{0,2})/);
401
+ const hours = timeParts[1] * 1;
402
+ obj.hour -= hours;
403
+ const mins = timeParts.length > 2 ? timeParts[2] * 1 : 0;
404
+ obj.minute -= (hours < 0) ? - mins : mins;
405
+ }]);
406
+ } else if (option !== 'timeZoneName') {
407
+ _bUseUTC = false
408
+ regexParts.push('.+?');
409
+ }
410
+
411
+ return regexParts;
412
+ }, []);
413
+ const regex = new RegExp(regexParts.join(''));
414
+ const match = dateString.match(regex);
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
418
+ const dateObj = {year: 1972, month: 0, day: 1, hour: 0, minute: 0, second: 0, fractionalSecondDigits: 0};
419
+ match.slice(1).forEach((m, index) => {
420
+ const [element, func] = lookups[index];
421
+ dateObj[element] = func(m, dateObj);
422
+ });
423
+ if (hourCycle === 'h24' && dateObj.hour === 24) dateObj.hour = 0;
424
+ if (hourCycle === 'h12' && dateObj.hour === 12) dateObj.hour = 0;
425
+ if (_bUseUTC) {
426
+ const utcDate = new Date(Date.UTC(
427
+ dateObj.year,
428
+ dateObj.month,
429
+ dateObj.day,
430
+ dateObj.hour,
431
+ dateObj.minute,
432
+ dateObj.second,
433
+ dateObj.fractionalSecondDigits,
434
+ ));
435
+ if (_setFullYear) {
436
+ utcDate.setUTCFullYear(dateObj.year);
437
+ }
438
+ return utcDate;
439
+ }
440
+ const jsDate = new Date(
441
+ dateObj.year,
442
+ dateObj.month,
443
+ dateObj.day,
444
+ dateObj.hour,
445
+ dateObj.minute,
446
+ dateObj.second,
447
+ dateObj.fractionalSecondDigits,
448
+ );
449
+ if (_setFullYear) {
450
+ jsDate.setFullYear(dateObj.year);
451
+ }
452
+ return timeZone == null ? jsDate : adjustTimeZone(jsDate, timeZone);
453
+ }
454
+
455
+ export function parseDefaultDate(dateString, language, bUseUTC) {
456
+ return parseDate(dateString, language, 'short', null, false);
457
+ }