lightning-base-components 1.15.3-alpha → 1.15.4-alpha
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,1810 @@
|
|
|
1
|
+
import commonDigits from '@salesforce/i18n/common.digits';
|
|
2
|
+
import commonCalendarData from '@salesforce/i18n/common.calendarData';
|
|
3
|
+
import defaultCalendar from '@salesforce/i18n/defaultCalendar';
|
|
4
|
+
import defaultNumberingSystem from '@salesforce/i18n/defaultNumberingSystem';
|
|
5
|
+
import calendarData from '@salesforce/i18n/calendarData';
|
|
6
|
+
import decimalSeparator from '@salesforce/i18n/number.decimalSeparator';
|
|
7
|
+
import groupingSeparator from '@salesforce/i18n/number.groupingSeparator';
|
|
8
|
+
import percentSign from '@salesforce/i18n/number.percentSign';
|
|
9
|
+
import plusSign from '@salesforce/i18n/number.plusSign';
|
|
10
|
+
import minusSign from '@salesforce/i18n/number.minusSign';
|
|
11
|
+
import exponentialSign from '@salesforce/i18n/number.exponentialSign';
|
|
12
|
+
import superscriptingExponentSign from '@salesforce/i18n/number.superscriptingExponentSign';
|
|
13
|
+
import perMilleSign from '@salesforce/i18n/number.perMilleSign';
|
|
14
|
+
import infinity from '@salesforce/i18n/number.infinity';
|
|
15
|
+
import nan from '@salesforce/i18n/number.nan';
|
|
16
|
+
import currencySymbol from '@salesforce/i18n/number.currencySymbol';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Given an object instance, generate an object with the same properties, with property name ordered alphabetically, and undefined properties removed.
|
|
20
|
+
*
|
|
21
|
+
* @param value Object to process
|
|
22
|
+
* @returns Generated object with properties only
|
|
23
|
+
*/
|
|
24
|
+
function extractProperties(value) {
|
|
25
|
+
// Return primitive value as is
|
|
26
|
+
const valueType = typeof value;
|
|
27
|
+
if (valueType === 'string' ||
|
|
28
|
+
valueType === 'number' ||
|
|
29
|
+
valueType === 'boolean' ||
|
|
30
|
+
valueType === 'undefined') {
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
if (valueType === 'object') {
|
|
34
|
+
// Array is an object
|
|
35
|
+
if (Array.isArray(value)) {
|
|
36
|
+
const newValue = [];
|
|
37
|
+
// Keep sort order
|
|
38
|
+
for (const arrayValue of value) {
|
|
39
|
+
newValue.push(extractProperties(arrayValue));
|
|
40
|
+
}
|
|
41
|
+
return newValue;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const newValue = {};
|
|
45
|
+
// Normalize property order by sorting it before assignment
|
|
46
|
+
const propNames = Object.getOwnPropertyNames(value).sort();
|
|
47
|
+
for (const propName of propNames) {
|
|
48
|
+
const propValue = extractProperties(value[propName]);
|
|
49
|
+
// Normalize property value by assigning only defined values to properties
|
|
50
|
+
if (propValue !== undefined) {
|
|
51
|
+
newValue[propName] = propValue;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return newValue;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* A Map-like object with normalized key
|
|
61
|
+
*/
|
|
62
|
+
class Cache {
|
|
63
|
+
/**
|
|
64
|
+
* Constructs an empty instance.
|
|
65
|
+
*
|
|
66
|
+
* @constructor
|
|
67
|
+
*/
|
|
68
|
+
constructor() {
|
|
69
|
+
/** Internal map object */
|
|
70
|
+
this.map = new Map();
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Checks whether this cache contains provided key.
|
|
74
|
+
*
|
|
75
|
+
* @param key Cache key
|
|
76
|
+
* @returns 'true' if cache has this key
|
|
77
|
+
*/
|
|
78
|
+
has(key) {
|
|
79
|
+
const newKey = this.normalize(key);
|
|
80
|
+
return this.map.has(newKey);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Returns the cached value for provided key, or undefined if key is unavailable
|
|
84
|
+
*
|
|
85
|
+
* @param key Cache key
|
|
86
|
+
* @returns Cache value
|
|
87
|
+
*/
|
|
88
|
+
get(key) {
|
|
89
|
+
const newKey = this.normalize(key);
|
|
90
|
+
return this.map.get(newKey);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Sets provided value to provided cache key. Current cached value will be overwritten if cache key exists.
|
|
94
|
+
*
|
|
95
|
+
* @param key Cache key. Examples: {prop: 'prop value', nested: { nestedProp: 'nestedProp value' }}
|
|
96
|
+
* @param value Cache value
|
|
97
|
+
* @returns Current cache instance
|
|
98
|
+
*/
|
|
99
|
+
set(key, value) {
|
|
100
|
+
const newKey = this.normalize(key);
|
|
101
|
+
this.map.set(newKey, value);
|
|
102
|
+
return this;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Removes all cached values.
|
|
106
|
+
*
|
|
107
|
+
* @returns void
|
|
108
|
+
*/
|
|
109
|
+
clear() {
|
|
110
|
+
return this.map.clear();
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Given a cache key, normalize the key to a string.
|
|
114
|
+
*
|
|
115
|
+
* @param key Cache key
|
|
116
|
+
* @returns Normalized cached key string
|
|
117
|
+
*/
|
|
118
|
+
normalize(key) {
|
|
119
|
+
const newKey = extractProperties(key);
|
|
120
|
+
return JSON.stringify(newKey);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const dateTimeFormatCache = new Map();
|
|
125
|
+
/**
|
|
126
|
+
* Clears the cache
|
|
127
|
+
*/
|
|
128
|
+
function clearDateTimeFormatCache() {
|
|
129
|
+
dateTimeFormatCache.clear();
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Instantiate or returns a cached value of {@link Intl.DateTimeFormat}, instantiated with given options.
|
|
133
|
+
*
|
|
134
|
+
* @param locale {string} Locale
|
|
135
|
+
* @param options {Intl.DateTimeFormatOptions} Formatter options
|
|
136
|
+
* @returns {Intl.DateTimeFormat} Formatter instance
|
|
137
|
+
*/
|
|
138
|
+
function getDateTimeFormat(locale = 'en-US', options = {}) {
|
|
139
|
+
if (!dateTimeFormatCache.has(locale)) {
|
|
140
|
+
dateTimeFormatCache.set(locale, new Cache());
|
|
141
|
+
}
|
|
142
|
+
if (!dateTimeFormatCache.get(locale).has(options)) {
|
|
143
|
+
const instance = new Intl.DateTimeFormat(locale, options);
|
|
144
|
+
dateTimeFormatCache.get(locale).set(options, instance);
|
|
145
|
+
}
|
|
146
|
+
return dateTimeFormatCache.get(locale).get(options);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Escapes a string to use in Javascript regular expression
|
|
151
|
+
*
|
|
152
|
+
* @param value String value to include in a regex
|
|
153
|
+
* @returns {string} Escaped value
|
|
154
|
+
*/
|
|
155
|
+
function escapeRegex(value) {
|
|
156
|
+
return value.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
157
|
+
}
|
|
158
|
+
/** Map of calendar name used on Intl to its CLDR counterpart */
|
|
159
|
+
const intlCalendarMap = {
|
|
160
|
+
gregory: 'gregorian',
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Resolves Intl calendar name to CLDR calendar name
|
|
164
|
+
*
|
|
165
|
+
* @param calendar {string} Calendar name for Intl
|
|
166
|
+
* @returns {string} Calendar name for CLDR
|
|
167
|
+
*/
|
|
168
|
+
function intlCalendarToCldr(calendar) {
|
|
169
|
+
return Object.prototype.hasOwnProperty.call(intlCalendarMap, calendar)
|
|
170
|
+
? intlCalendarMap[calendar]
|
|
171
|
+
: calendar;
|
|
172
|
+
}
|
|
173
|
+
const eraDateRegex = /^([-]?[0-9]+)-([0-9]{1,2})-([0-9]{1,2})$/;
|
|
174
|
+
/**
|
|
175
|
+
* Parses the date string on CLDR era start/end data to year/month/day property object.
|
|
176
|
+
*
|
|
177
|
+
* @private
|
|
178
|
+
* @param value {string} Era date value to parse
|
|
179
|
+
* @returns Resolved date value
|
|
180
|
+
*/
|
|
181
|
+
function parseEraDate(value) {
|
|
182
|
+
if (value && eraDateRegex.test(value)) {
|
|
183
|
+
const matches = value.match(eraDateRegex);
|
|
184
|
+
return {
|
|
185
|
+
year: parseInt(matches[0], 10),
|
|
186
|
+
month: parseInt(matches[0], 10),
|
|
187
|
+
day: parseInt(matches[0], 10),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
// function compareDate(l: { year: number, month: number, day: number }, r: { year: number, month: number, day: number }): number {
|
|
193
|
+
// if (l.year < r.year || (l.year === r.year && l.month < r.month) || (l.year === r.year && l.month === r.month && l.day < r.day)) return -1;
|
|
194
|
+
// if (l.year > r.year || (l.year === r.year && l.month > r.month) || (l.year === r.year && l.month === r.month && l.day > r.day)) return 1;
|
|
195
|
+
// return 0;
|
|
196
|
+
// }
|
|
197
|
+
/**
|
|
198
|
+
* Resolves a date with year that is indexed to a specific era to its gregorian year value. e.g. Reiwa 3, January 1st for Japanese imperial calendar will resolve to 2021
|
|
199
|
+
*
|
|
200
|
+
* @param eraIndex {number} Era index
|
|
201
|
+
* @param year {number} Year in era
|
|
202
|
+
* @param month {number} Month in era
|
|
203
|
+
* @param day {number} Day in era
|
|
204
|
+
* @param calendarType {string} Calendar type. e.g. "gregorian"
|
|
205
|
+
* @param common {LocaleCommonDataCalendarData} Locale common calendar data
|
|
206
|
+
* @returns {number} Positive gregorian year for AD, negative gregorian year for BC
|
|
207
|
+
*/
|
|
208
|
+
function getGregorianYear(eraIndex, year, month, day, calendarType = 'gregorian', calendarData) {
|
|
209
|
+
if (calendarType === 'gregorian') {
|
|
210
|
+
// Fast path for gregorian
|
|
211
|
+
// eraIndex === 0 if BC. A negative BC is a positive AD, vice versa
|
|
212
|
+
return eraIndex === 0 ? -year : year;
|
|
213
|
+
}
|
|
214
|
+
else if (calendarData && calendarData.calendarSystem === 'solar' && calendarData.eras) {
|
|
215
|
+
// non-gregorian solar calendars
|
|
216
|
+
const eras = Object.keys(calendarData.eras)
|
|
217
|
+
.sort()
|
|
218
|
+
.map((k) => calendarData.eras[k]);
|
|
219
|
+
if (eraIndex >= eras.length || eraIndex < 0) {
|
|
220
|
+
// NOTE: Should we start with era that match today, rather than last era if invalid?
|
|
221
|
+
eraIndex = eras.length - 1;
|
|
222
|
+
}
|
|
223
|
+
const era = eras[eraIndex];
|
|
224
|
+
const yearAdjusted = year > 0 ? year - 1 : year < 0 ? year + 1 : 0;
|
|
225
|
+
// We can only have either _start or _end on era
|
|
226
|
+
if (era._end) {
|
|
227
|
+
// This can only be the first era (eras[0]) that counts down (gregorian, coptic, ethiopic)
|
|
228
|
+
const eraEnd = parseEraDate(era._end);
|
|
229
|
+
if (!eraEnd)
|
|
230
|
+
return undefined; // Report to Unicode
|
|
231
|
+
return eraEnd.year - yearAdjusted;
|
|
232
|
+
}
|
|
233
|
+
else if (era._start) {
|
|
234
|
+
const eraStart = parseEraDate(era._start);
|
|
235
|
+
if (!eraStart)
|
|
236
|
+
return undefined; // Report to Unicode
|
|
237
|
+
return eraStart.year + yearAdjusted;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
else if (calendarData && calendarData.calendarSystem !== 'solar' && calendarData.eras) {
|
|
241
|
+
// TODO: non-gregorian non-solar calendars
|
|
242
|
+
return year;
|
|
243
|
+
}
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Extracts localized digits on a string and convert it to an array of integers
|
|
248
|
+
*
|
|
249
|
+
* @param value {string} String value
|
|
250
|
+
* @param localeDigits {string[]} Array of localized digits. e.g. ["0",...,"9"] for "latn"
|
|
251
|
+
* @returns {number[]} Array of integers from string. e.g. [1,2,3] for "123" for "latn"
|
|
252
|
+
*/
|
|
253
|
+
function stringToDigits(value, localeDigits) {
|
|
254
|
+
const digits = [];
|
|
255
|
+
[...value].forEach((char) => {
|
|
256
|
+
const digit = localeDigits.indexOf(char);
|
|
257
|
+
if (digit >= 0) {
|
|
258
|
+
digits.push(digit);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
return digits.reverse();
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Extracts localized digits on a string and convert it to an integer
|
|
265
|
+
*
|
|
266
|
+
* @param value {string} String value
|
|
267
|
+
* @param digits {string} Localized digits. e.g. "0123456789" for "latn"
|
|
268
|
+
* @returns {number} Integer value. e.g. 123 for "123" for "latn"
|
|
269
|
+
*/
|
|
270
|
+
function parseDigits(value, digits) {
|
|
271
|
+
return stringToDigits(value, Array.from(digits)).reduce((int, cur, idx) => (int += cur * 10 ** idx), 0);
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* @private
|
|
275
|
+
*/
|
|
276
|
+
function getDigitsRegexPatternInternal(digits, extras = '') {
|
|
277
|
+
// Optimze for sequential codepoints
|
|
278
|
+
if (digits.length > 1 &&
|
|
279
|
+
digits.reduce((sum, digit, idx, arr) => idx === 0
|
|
280
|
+
? true
|
|
281
|
+
: sum && digit.codePointAt(0) - arr[idx - 1].codePointAt(0) === 1, false)) {
|
|
282
|
+
return '[' + [digits[0], digits[digits.length - 1]].join('-') + escapeRegex(extras) + ']';
|
|
283
|
+
}
|
|
284
|
+
return '[' + digits.join('') + escapeRegex(extras) + ']';
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Generates an optimized regular expression for given localized digits.
|
|
288
|
+
* We would want a regular expression of [0-9] instead of [0123456789],
|
|
289
|
+
* or [\u{660}-\u{669}] instead of [\u{660}\u{661}\u{662}\u{663}\u{664}\u{665}\u{666}\u{667}\u{668}\u{669}] for arabic.
|
|
290
|
+
*
|
|
291
|
+
* @param digits {string} Localized digit string. e.g. "0123456789" for "latn"
|
|
292
|
+
* @param extras {string} Extra characters to include in regular expression range
|
|
293
|
+
* @returns {string} Optimized regular expression for given digits
|
|
294
|
+
*/
|
|
295
|
+
function getDigitsRegexPattern(digits, extras = '') {
|
|
296
|
+
return getDigitsRegexPatternInternal(Array.from(digits), extras);
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* This formats to "MM/dd/yyyy, hh:mm" in English (United States) specifically
|
|
300
|
+
*/
|
|
301
|
+
const UTC_FORMAT = new Intl.DateTimeFormat('en-US', {
|
|
302
|
+
timeZone: 'UTC',
|
|
303
|
+
hourCycle: 'h23',
|
|
304
|
+
year: 'numeric',
|
|
305
|
+
month: 'numeric',
|
|
306
|
+
day: 'numeric',
|
|
307
|
+
hour: 'numeric',
|
|
308
|
+
minute: 'numeric',
|
|
309
|
+
});
|
|
310
|
+
/**
|
|
311
|
+
* This matches for "MM/dd/yyyy, hh:mm"
|
|
312
|
+
* Group 0: month
|
|
313
|
+
* Group 1: date
|
|
314
|
+
* Group 2: year
|
|
315
|
+
* Group 3: hour
|
|
316
|
+
* Group 4: minutes
|
|
317
|
+
*/
|
|
318
|
+
const EN_US_DATE_REGEXP = /(\d+).(\d+).(\d+),?\s+(\d+).(\d+)(.(\d+))?/;
|
|
319
|
+
const EN_US_DATE_INDEX_DAY = 1;
|
|
320
|
+
const EN_US_DATE_INDEX_HOUR = 3;
|
|
321
|
+
const EN_US_DATE_INDEX_MINUTE = 4;
|
|
322
|
+
const TIMEZONE_DATEFORMATS = new Map();
|
|
323
|
+
/**
|
|
324
|
+
* Parses date string in specific "MM/dd/yyyy, hh:mm" format to its respected parts.
|
|
325
|
+
*
|
|
326
|
+
* @param value Date string value in "MM/dd/yyyy, hh:mm" format
|
|
327
|
+
* @returns Array of parsed strings based on groups defined at EN_US_DATE_REGEXP
|
|
328
|
+
*/
|
|
329
|
+
function parseEnUsDate(value) {
|
|
330
|
+
const dateString = value.replace(/[\u200E\u200F]/g, ''); // Cleanse
|
|
331
|
+
return [].slice.call(EN_US_DATE_REGEXP.exec(dateString), 1).map(Math.floor);
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Given two date parts parsed by parseEnUsDate, return the difference in minutes.
|
|
335
|
+
*
|
|
336
|
+
* @param date1 Date parts
|
|
337
|
+
* @param date2 Date parts
|
|
338
|
+
* @returns Difference in minutes
|
|
339
|
+
*/
|
|
340
|
+
function diffMinutes(date1, date2) {
|
|
341
|
+
let day = date1[EN_US_DATE_INDEX_DAY] - date2[EN_US_DATE_INDEX_DAY];
|
|
342
|
+
const hour = date1[EN_US_DATE_INDEX_HOUR] - date2[EN_US_DATE_INDEX_HOUR];
|
|
343
|
+
const min = date1[EN_US_DATE_INDEX_MINUTE] - date2[EN_US_DATE_INDEX_MINUTE];
|
|
344
|
+
if (day > 15) {
|
|
345
|
+
day = -1;
|
|
346
|
+
}
|
|
347
|
+
if (day < -15) {
|
|
348
|
+
day = 1;
|
|
349
|
+
}
|
|
350
|
+
return 60 * (24 * day + hour) + min;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Return timezone offset of given date in minutes
|
|
354
|
+
*
|
|
355
|
+
* @param timeZoneId Time zone
|
|
356
|
+
* @param date Date object
|
|
357
|
+
* @returns Number of time offset in minutes
|
|
358
|
+
*/
|
|
359
|
+
function getTimeZoneOffset(timeZone, date) {
|
|
360
|
+
if (!TIMEZONE_DATEFORMATS.has(timeZone)) {
|
|
361
|
+
// This must be the same locale (English (United States)) and options as UTC_FORMAT, minus the timeZone difference
|
|
362
|
+
TIMEZONE_DATEFORMATS.set(timeZone, new Intl.DateTimeFormat('en-US', {
|
|
363
|
+
timeZone: timeZone,
|
|
364
|
+
hourCycle: 'h23',
|
|
365
|
+
year: 'numeric',
|
|
366
|
+
month: 'numeric',
|
|
367
|
+
day: 'numeric',
|
|
368
|
+
hour: 'numeric',
|
|
369
|
+
minute: 'numeric',
|
|
370
|
+
}));
|
|
371
|
+
}
|
|
372
|
+
const dateFormat = TIMEZONE_DATEFORMATS.get(timeZone);
|
|
373
|
+
return diffMinutes(parseEnUsDate(UTC_FORMAT.format(date)), parseEnUsDate(dateFormat.format(date)));
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Validates week
|
|
377
|
+
*
|
|
378
|
+
* @param week {Number} Week number
|
|
379
|
+
* @returns {boolean} true if the week is valid
|
|
380
|
+
*/
|
|
381
|
+
function isValidWeek(week) {
|
|
382
|
+
return (Number.isInteger(week) && week >= 1 /*week starts at 1 */ && week <= 53); /* at most we only have 53 weeks */
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Validates day of week
|
|
386
|
+
*
|
|
387
|
+
* @param dayOfWeek {Number} dayOfWeek number
|
|
388
|
+
* @returns {boolean} true if the day of week is valid
|
|
389
|
+
*/
|
|
390
|
+
function isValidDayOfWeek(dayOfWeek) {
|
|
391
|
+
return (Number.isInteger(dayOfWeek) &&
|
|
392
|
+
dayOfWeek >= 1 /* day of week starts at 1 */ &&
|
|
393
|
+
dayOfWeek <= 7); /* day of week ends at 7 */
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Validates month
|
|
397
|
+
*
|
|
398
|
+
* @param month {Number} Month number
|
|
399
|
+
* @returns {boolean} true if the month is valid
|
|
400
|
+
*/
|
|
401
|
+
function isValidMonth(month) {
|
|
402
|
+
return is31DayMonth(month) || is30DayMonth(month) || month === 2;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Check if the month has 31 days
|
|
406
|
+
*
|
|
407
|
+
* @param month {Number} Month number
|
|
408
|
+
* @returns {boolean} true if the month has 31 days
|
|
409
|
+
*/
|
|
410
|
+
function is31DayMonth(month) {
|
|
411
|
+
return (month === 1 ||
|
|
412
|
+
month === 3 ||
|
|
413
|
+
month === 5 ||
|
|
414
|
+
month === 7 ||
|
|
415
|
+
month === 8 ||
|
|
416
|
+
month === 10 ||
|
|
417
|
+
month === 12);
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Check if the month has 30 days
|
|
421
|
+
*
|
|
422
|
+
* @param month {Number} Month number
|
|
423
|
+
* @returns {boolean} true if the month has 30 days
|
|
424
|
+
*/
|
|
425
|
+
function is30DayMonth(month) {
|
|
426
|
+
return month === 4 || month === 6 || month === 9 || month === 11;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Check if ordinal is valid
|
|
430
|
+
*
|
|
431
|
+
* @param ordinal {Number} Ordinal number
|
|
432
|
+
* @param isLeapYear {boolean}
|
|
433
|
+
* @returns {boolean} true if the ordinal is valid
|
|
434
|
+
*/
|
|
435
|
+
function isValidOrdinal(ordinal, isLeapYear) {
|
|
436
|
+
return (Number.isInteger(ordinal) &&
|
|
437
|
+
ordinal >= 1 /* ordinal day starts at 1 */ &&
|
|
438
|
+
((isLeapYear && ordinal <= 366) /* at most we only have 366 days for leap year */ ||
|
|
439
|
+
(!isLeapYear && ordinal <= 365))); /* at most we only have 366 days for leap year */
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Validate date
|
|
443
|
+
*
|
|
444
|
+
* @param month {Number} Month number
|
|
445
|
+
* @param day {Number} Day number
|
|
446
|
+
* @param isLeapYear {boolean} Indicates whether its a leap year
|
|
447
|
+
* @returns {boolean} true if the date is valid
|
|
448
|
+
*/
|
|
449
|
+
function isValidDate(month, day, isLeapYear) {
|
|
450
|
+
return (isValidMonth(month) &&
|
|
451
|
+
Number.isInteger(day) &&
|
|
452
|
+
day >= 1 /* 0 is not a date */ &&
|
|
453
|
+
((day <= 30 && is30DayMonth(month)) /* validation for 30 day months */ ||
|
|
454
|
+
(day <= 31 && is31DayMonth(month)) /* validation for 31 day months */ ||
|
|
455
|
+
(isLeapYear && month === 2 && day <= 29) /* february, leap year */ ||
|
|
456
|
+
(!isLeapYear && month === 2 && day <= 28))); /* february, non leap year */
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Calendar holds broken down data of an instance in time to calendar parts (year, month, day, etc). This helps in calendar arithmetic (e.g. change time zones, add days, etc)
|
|
461
|
+
*/
|
|
462
|
+
class Calendar {
|
|
463
|
+
/**
|
|
464
|
+
* @constructor
|
|
465
|
+
* @param timeZone {string} Time zone of this instance
|
|
466
|
+
*/
|
|
467
|
+
constructor(timeZone) {
|
|
468
|
+
this.year = 0;
|
|
469
|
+
this.month = 1;
|
|
470
|
+
this.day = 1;
|
|
471
|
+
this.hour = 0;
|
|
472
|
+
this.minute = 0;
|
|
473
|
+
this.second = 0;
|
|
474
|
+
this.millisecond = 0;
|
|
475
|
+
this.timeZone = timeZone;
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Clears the data on this calendar instance
|
|
479
|
+
*/
|
|
480
|
+
clear() {
|
|
481
|
+
this.year = 0;
|
|
482
|
+
this.month = 1;
|
|
483
|
+
this.day = 1;
|
|
484
|
+
this.hour = 0;
|
|
485
|
+
this.minute = 0;
|
|
486
|
+
this.second = 0;
|
|
487
|
+
this.millisecond = 0;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Converts this calendar instance to an equivalent Javascript Date instance
|
|
491
|
+
*
|
|
492
|
+
* @param smallYearOffset {number} Offset year to use if this calendar year is a two-digit year
|
|
493
|
+
* @returns {Date} Date instance
|
|
494
|
+
*/
|
|
495
|
+
getDate(smallYearOffset = 2000) {
|
|
496
|
+
// Adjust year if two digits and below
|
|
497
|
+
if (this.year < 100 && this.year >= 0) {
|
|
498
|
+
this.year += smallYearOffset;
|
|
499
|
+
}
|
|
500
|
+
// Date is constructed with date & time in local (browser) time zone
|
|
501
|
+
const localDate = new Date(this.year, this.month - 1, this.day, this.hour, this.minute, this.second, this.millisecond);
|
|
502
|
+
if (this.timeZone) {
|
|
503
|
+
const localOffset = localDate.getTimezoneOffset();
|
|
504
|
+
const timeZoneOffset = getTimeZoneOffset(this.timeZone, localDate);
|
|
505
|
+
const offset = localOffset - timeZoneOffset;
|
|
506
|
+
// Return local date if offset equals, else adjust millisecond and return adjusted date
|
|
507
|
+
if (Math.floor(offset) !== 0) {
|
|
508
|
+
const adjustedDate = new Date(localDate.getTime() - offset * 60 * 1000);
|
|
509
|
+
return adjustedDate;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return localDate;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Tokenize string value of CLDR date time format string to array of tokens.
|
|
518
|
+
*
|
|
519
|
+
* Ref: https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns
|
|
520
|
+
*
|
|
521
|
+
* @param value CLDR date time format pattern
|
|
522
|
+
* @returns Array of tokens
|
|
523
|
+
*/
|
|
524
|
+
function tokenizeDateTimePattern(value) {
|
|
525
|
+
// Notes regarding single quote escape:
|
|
526
|
+
// <quote>
|
|
527
|
+
// Literal text, which is output as-is when formatting, and must closely match when parsing. Literal text can include:
|
|
528
|
+
// * Any characters other than A..Z and a..z, including spaces and punctuation.
|
|
529
|
+
// * Any text between single vertical quotes ('xxxx'), which may include A..Z and a..z as literal text.
|
|
530
|
+
// * Two adjacent single vertical quotes (''), which represent a literal single quote, either inside or outside quoted text.
|
|
531
|
+
// </quote>
|
|
532
|
+
// Ref: https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns
|
|
533
|
+
// e.g.
|
|
534
|
+
// '' -> [text(')]
|
|
535
|
+
// 'a' -> [text(a)]
|
|
536
|
+
// 'ymd' -> [text(ymd)]
|
|
537
|
+
// ''a -> [text('), token(a)]
|
|
538
|
+
// ''' -> [text(')]
|
|
539
|
+
// '''' -> [text('')]
|
|
540
|
+
// 'a''b' -> [text(a'b)]
|
|
541
|
+
// ' '' ' -> [text( ' )]
|
|
542
|
+
const tokenValues = [...'GyYQqM<wWdDFEecabBhHKkmsSAxXOvVXxZz']; // DateTimePatternToken.type
|
|
543
|
+
const tokens = [];
|
|
544
|
+
let isEscapedText = false;
|
|
545
|
+
let isPrevSingleQuote = false;
|
|
546
|
+
[...value].forEach((char) => {
|
|
547
|
+
// tokenValue is defined if it is a known token character.
|
|
548
|
+
const tokenValue = tokenValues.indexOf(char) >= 0 ? char : undefined;
|
|
549
|
+
let isCharText = false;
|
|
550
|
+
// Set single quoted mode
|
|
551
|
+
if ((tokenValue !== undefined || char !== "'") && isPrevSingleQuote) {
|
|
552
|
+
isEscapedText = !isEscapedText;
|
|
553
|
+
}
|
|
554
|
+
// Process
|
|
555
|
+
if (tokenValue !== undefined && !isEscapedText) {
|
|
556
|
+
if (tokens.length > 0 && tokens[tokens.length - 1].type === tokenValue) {
|
|
557
|
+
// Current character is same as previous token. extending token length
|
|
558
|
+
let tokenLength = tokens[tokens.length - 1].length;
|
|
559
|
+
tokenLength = tokenLength === undefined ? 1 : tokenLength + 1;
|
|
560
|
+
tokens[tokens.length - 1].length = tokenLength;
|
|
561
|
+
}
|
|
562
|
+
else if (tokenValue !== undefined) {
|
|
563
|
+
// Current character is a known token
|
|
564
|
+
tokens.push({
|
|
565
|
+
// @ts-ignore
|
|
566
|
+
type: tokenValue,
|
|
567
|
+
length: 1,
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
isPrevSingleQuote = false;
|
|
571
|
+
}
|
|
572
|
+
else if (char === "'" && !isPrevSingleQuote) {
|
|
573
|
+
isPrevSingleQuote = true;
|
|
574
|
+
}
|
|
575
|
+
else if (char === "'" && isPrevSingleQuote) {
|
|
576
|
+
// Treat two single quotes as one single quote text
|
|
577
|
+
isCharText = true;
|
|
578
|
+
isPrevSingleQuote = false; // Consume current single quote
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
// Treat others as text
|
|
582
|
+
isCharText = true;
|
|
583
|
+
isPrevSingleQuote = false; // Consumed current single quote
|
|
584
|
+
}
|
|
585
|
+
if (isCharText) {
|
|
586
|
+
// Add as text token
|
|
587
|
+
// Extend last token if it is a text token
|
|
588
|
+
if (tokens.length > 0 && tokens[tokens.length - 1].type === undefined) {
|
|
589
|
+
// Previous token is text, extending token text
|
|
590
|
+
let tokenText = tokens[tokens.length - 1].text;
|
|
591
|
+
tokenText = tokenText === undefined ? char : tokenText + char;
|
|
592
|
+
tokens[tokens.length - 1].text = tokenText;
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
// Previous token is not text, creating new text token
|
|
596
|
+
tokens.push({
|
|
597
|
+
text: char,
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
return tokens;
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Gets symbols for given CLDR date time format token from CLDR locale data.
|
|
606
|
+
*
|
|
607
|
+
* @param token CLDR date time format token
|
|
608
|
+
* @param calendarData Locale data for calendar
|
|
609
|
+
* @returns Map of symbols from locale data for given token. e.g. {'0':'january','1':'february',...}
|
|
610
|
+
*/
|
|
611
|
+
function getSymbols(token, calendarData) {
|
|
612
|
+
switch (token) {
|
|
613
|
+
case 'a':
|
|
614
|
+
case 'aa':
|
|
615
|
+
case 'aaa':
|
|
616
|
+
return calendarData.dayPeriods.format.abbreviated;
|
|
617
|
+
case 'aaaa':
|
|
618
|
+
return calendarData.dayPeriods.format.wide;
|
|
619
|
+
case 'aaaaa':
|
|
620
|
+
return calendarData.dayPeriods.format.narrow;
|
|
621
|
+
case 'G':
|
|
622
|
+
return calendarData.eras.eraAbbr;
|
|
623
|
+
case 'GGGG':
|
|
624
|
+
return calendarData.eras.eraNames;
|
|
625
|
+
case 'GGGGG':
|
|
626
|
+
return calendarData.eras.eraNarrow;
|
|
627
|
+
case 'MMM':
|
|
628
|
+
return calendarData.months.format.abbreviated;
|
|
629
|
+
case 'MMMM':
|
|
630
|
+
return calendarData.months.format.wide;
|
|
631
|
+
case 'MMMMM':
|
|
632
|
+
return calendarData.months.format.narrow;
|
|
633
|
+
case 'EEEE':
|
|
634
|
+
return calendarData.days.format.wide;
|
|
635
|
+
case 'EEEEE':
|
|
636
|
+
return calendarData.days.format.narrow;
|
|
637
|
+
case 'EEEEEE':
|
|
638
|
+
return calendarData.days.format.abbreviated;
|
|
639
|
+
default:
|
|
640
|
+
return {};
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Gets the token type for CLDR date time format token, whether it is numeric, specific digit numeric, or string
|
|
645
|
+
*
|
|
646
|
+
* @param value CLDR date time format token
|
|
647
|
+
* @returns -1 if token is not numeric, 0 if token is of arbitrary digits, >= 1 if token is of specific digit
|
|
648
|
+
*/
|
|
649
|
+
function getTokenDigits(token) {
|
|
650
|
+
switch (token.type) {
|
|
651
|
+
case 'y': // Year
|
|
652
|
+
case 'd': // DayOfMonth
|
|
653
|
+
case 'K': // Hour0011
|
|
654
|
+
case 'H': // Hour0023
|
|
655
|
+
case 'h': // Hour0112
|
|
656
|
+
case 'k': // Hour0124
|
|
657
|
+
case 'm': // Minute
|
|
658
|
+
case 's': // Second
|
|
659
|
+
case 'S': // SecondFractional
|
|
660
|
+
case 'A': // MilliSecond
|
|
661
|
+
// eslint-disable-next-line no-fallthrough
|
|
662
|
+
case 'Q': // Quarter
|
|
663
|
+
return token.length === 2 ? 2 : 0;
|
|
664
|
+
case 'M': // Month
|
|
665
|
+
case 'e': // WeekDayLocal
|
|
666
|
+
case 'c': // WeekDayLocalStandalone
|
|
667
|
+
switch (token.length) {
|
|
668
|
+
case 1:
|
|
669
|
+
return 0;
|
|
670
|
+
case 2:
|
|
671
|
+
return 2;
|
|
672
|
+
default:
|
|
673
|
+
return -1;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
return -1;
|
|
677
|
+
}
|
|
678
|
+
/** Cache regex */
|
|
679
|
+
const regexps = new Map();
|
|
680
|
+
/**
|
|
681
|
+
* Generates the expression object that contains regex and tokens used on the regex groups based on given pattern and locale data.
|
|
682
|
+
*
|
|
683
|
+
* @param pattern CLDR date time format pattern
|
|
684
|
+
* @param digits Used digits
|
|
685
|
+
* @param calData Locale data for calendar
|
|
686
|
+
* @returns DateTimePattenExpression
|
|
687
|
+
*/
|
|
688
|
+
function getExpression$1(pattern, digits, calData) {
|
|
689
|
+
const tokens = tokenizeDateTimePattern(pattern);
|
|
690
|
+
const groups = [];
|
|
691
|
+
const digitsRange = getDigitsRegexPattern(digits);
|
|
692
|
+
let exprValue = '^[\\s]*'; // trim leading whitespace
|
|
693
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
694
|
+
const token = tokens[i];
|
|
695
|
+
if (token.text !== undefined && token.text !== '') {
|
|
696
|
+
// text
|
|
697
|
+
exprValue += '(' + escapeRegex(token.text) + ')';
|
|
698
|
+
groups.push(token);
|
|
699
|
+
}
|
|
700
|
+
else if (token.type !== undefined) {
|
|
701
|
+
const tokenString = token.type.repeat(token.length);
|
|
702
|
+
const digits = getTokenDigits(token);
|
|
703
|
+
if (digits === -1) {
|
|
704
|
+
if (tokenString === 'z') {
|
|
705
|
+
// time zone is free text w/o whitespace
|
|
706
|
+
exprValue += '([^\\s]+)';
|
|
707
|
+
}
|
|
708
|
+
else {
|
|
709
|
+
// non-numeric symbols
|
|
710
|
+
const symbols = getSymbols(tokenString, calData);
|
|
711
|
+
exprValue +=
|
|
712
|
+
'(' +
|
|
713
|
+
Object.keys(symbols)
|
|
714
|
+
.map((k) => escapeRegex(symbols[k]))
|
|
715
|
+
.join('|') +
|
|
716
|
+
')';
|
|
717
|
+
}
|
|
718
|
+
groups.push(token);
|
|
719
|
+
}
|
|
720
|
+
else if (digits === 0) {
|
|
721
|
+
// arbitrary digits
|
|
722
|
+
exprValue += '(' + digitsRange + '+)';
|
|
723
|
+
groups.push(token);
|
|
724
|
+
}
|
|
725
|
+
else if (digits > 0) {
|
|
726
|
+
// exact digits
|
|
727
|
+
exprValue += '(' + digitsRange + '{1,' + digits + '})';
|
|
728
|
+
groups.push(token);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
exprValue += '[\\s]*$'; // trim trailing whitespace
|
|
733
|
+
if (!regexps.has(exprValue)) {
|
|
734
|
+
regexps.set(exprValue, new RegExp(exprValue));
|
|
735
|
+
}
|
|
736
|
+
return { regexp: regexps.get(exprValue), groups };
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Utility to return the first key of object property by its value.
|
|
740
|
+
*
|
|
741
|
+
* @param parent Target object
|
|
742
|
+
* @param value Property value to find
|
|
743
|
+
* @returns Property key
|
|
744
|
+
*/
|
|
745
|
+
function getKeyByValue(parent, value) {
|
|
746
|
+
for (const key in parent) {
|
|
747
|
+
if (Object.prototype.hasOwnProperty.call(parent, key) && parent[key] === value) {
|
|
748
|
+
return key;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
return undefined;
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Utility to return the CLDR locale data key from locale data by its value.
|
|
755
|
+
* e.g. symbol='February', token={type:'M',length:'MMM'} returns '1'
|
|
756
|
+
*
|
|
757
|
+
* @param symbol Value to search
|
|
758
|
+
* @param token CLDR date time format token to search
|
|
759
|
+
* @param data Locale data for calendar
|
|
760
|
+
* @returns CLDR key name
|
|
761
|
+
*/
|
|
762
|
+
function getKeyFromSymbol(symbol, token, data) {
|
|
763
|
+
const tokenString = token.type.repeat(token.length);
|
|
764
|
+
switch (token.type) {
|
|
765
|
+
case 'G': // Era
|
|
766
|
+
switch (tokenString) {
|
|
767
|
+
case 'G':
|
|
768
|
+
return getKeyByValue(data.eras.eraNames, symbol);
|
|
769
|
+
case 'GGGG':
|
|
770
|
+
return getKeyByValue(data.eras.eraNarrow, symbol);
|
|
771
|
+
case 'GGGGG':
|
|
772
|
+
return getKeyByValue(data.eras.eraAbbr, symbol);
|
|
773
|
+
}
|
|
774
|
+
break;
|
|
775
|
+
case 'M': // Month
|
|
776
|
+
case 'L': // MonthStandalone
|
|
777
|
+
switch (tokenString) {
|
|
778
|
+
case 'MMM': // abbreviated/short
|
|
779
|
+
return getKeyByValue(data.months.format.abbreviated, symbol);
|
|
780
|
+
case 'MMMM': // wide/long
|
|
781
|
+
return getKeyByValue(data.months.format.wide, symbol);
|
|
782
|
+
case 'MMMMM': // narrow
|
|
783
|
+
return getKeyByValue(data.months.format.narrow, symbol);
|
|
784
|
+
case 'LLL': // abbreviated/short
|
|
785
|
+
return getKeyByValue(data.months['stand-alone'].short, symbol);
|
|
786
|
+
case 'LLLL': // wide/long
|
|
787
|
+
return getKeyByValue(data.months['stand-alone'].wide, symbol);
|
|
788
|
+
case 'LLLLL': // narrow
|
|
789
|
+
return getKeyByValue(data.months['stand-alone'].narrow, symbol);
|
|
790
|
+
}
|
|
791
|
+
break;
|
|
792
|
+
case 'a': // PeriodAmPm
|
|
793
|
+
case 'b': // PeriodAmPmNoonMidnight
|
|
794
|
+
case 'B': // PeriodFlexible
|
|
795
|
+
if (token.length >= 1 && token.length <= 3)
|
|
796
|
+
return getKeyByValue(data.dayPeriods.format.abbreviated, symbol);
|
|
797
|
+
if (token.length === 4)
|
|
798
|
+
return getKeyByValue(data.dayPeriods.format.wide, symbol);
|
|
799
|
+
if (token.length === 5)
|
|
800
|
+
return getKeyByValue(data.dayPeriods.format.narrow, symbol);
|
|
801
|
+
break;
|
|
802
|
+
case 'E': // WeekDay:
|
|
803
|
+
case 'e': // WeekDayLocal // Same keys as WeekDay. But index of monday changes per locale
|
|
804
|
+
case 'c': // WeekDayLocalStandalone: // Same keys as WeekDay. But index of monday changes per locale
|
|
805
|
+
switch (tokenString) {
|
|
806
|
+
// abbreviated
|
|
807
|
+
case 'E':
|
|
808
|
+
case 'EE':
|
|
809
|
+
case 'EEE':
|
|
810
|
+
case 'eee':
|
|
811
|
+
case 'ccc':
|
|
812
|
+
return getKeyByValue(data.days.format.abbreviated, symbol);
|
|
813
|
+
// wide/long
|
|
814
|
+
case 'EEEE':
|
|
815
|
+
case 'eeee':
|
|
816
|
+
case 'cccc':
|
|
817
|
+
return getKeyByValue(data.days.format.wide, symbol);
|
|
818
|
+
// narrow
|
|
819
|
+
case 'EEEEE':
|
|
820
|
+
case 'eeeee':
|
|
821
|
+
case 'ccccc':
|
|
822
|
+
return getKeyByValue(data.days.format.abbreviated, symbol);
|
|
823
|
+
// short
|
|
824
|
+
case 'EEEEEE':
|
|
825
|
+
case 'eeeeee':
|
|
826
|
+
case 'cccccc':
|
|
827
|
+
return getKeyByValue(data.days.format.short, symbol);
|
|
828
|
+
}
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
831
|
+
return undefined;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/*
|
|
835
|
+
* CLDR locale specific types
|
|
836
|
+
*/
|
|
837
|
+
/**
|
|
838
|
+
* Keys used on LocaleDataDatesCalendar for months
|
|
839
|
+
*/
|
|
840
|
+
const MONTHS_KEYS = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'];
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Date/time parser implementation based on CLDR locale data
|
|
844
|
+
*
|
|
845
|
+
* @author aazizy
|
|
846
|
+
*/
|
|
847
|
+
class DateTimeParseImpl {
|
|
848
|
+
/**
|
|
849
|
+
* @constructor
|
|
850
|
+
* @param options {DateTimeParseOptions} Options
|
|
851
|
+
*/
|
|
852
|
+
constructor(options) {
|
|
853
|
+
// Build data
|
|
854
|
+
const data = {
|
|
855
|
+
commonDigits,
|
|
856
|
+
commonCalendarData,
|
|
857
|
+
defaultCalendar,
|
|
858
|
+
defaultNumberingSystem,
|
|
859
|
+
calendarData,
|
|
860
|
+
};
|
|
861
|
+
// Resolve options
|
|
862
|
+
this.calendarType = options.calendar
|
|
863
|
+
? intlCalendarToCldr(options.calendar)
|
|
864
|
+
: data.defaultCalendar;
|
|
865
|
+
const numSys = options.numberingSystem
|
|
866
|
+
? options.numberingSystem
|
|
867
|
+
: data.defaultNumberingSystem;
|
|
868
|
+
const timeZone = options.timeZone ? options.timeZone : undefined;
|
|
869
|
+
options.numberingSystem = numSys;
|
|
870
|
+
options.timeZone = timeZone;
|
|
871
|
+
this.options = options;
|
|
872
|
+
this.digits = data.commonDigits[numSys];
|
|
873
|
+
this.commonCalendarData = data.commonCalendarData[this.calendarType];
|
|
874
|
+
this.calendarData = data.calendarData[this.calendarType];
|
|
875
|
+
// Generating expression here becuse we want to do this only once.
|
|
876
|
+
this.expr = getExpression$1(this.options.pattern, this.digits, this.calendarData);
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* {@inheritdoc}
|
|
880
|
+
*/
|
|
881
|
+
parse(value) {
|
|
882
|
+
const matches = value.match(this.expr.regexp);
|
|
883
|
+
if (!matches || matches.length <= this.expr.groups.length) {
|
|
884
|
+
throw new Error("Invalid date: '" + value + "' for pattern: '" + this.options.pattern + "'");
|
|
885
|
+
}
|
|
886
|
+
const cal = new Calendar(this.options.timeZone);
|
|
887
|
+
cal.clear();
|
|
888
|
+
let era;
|
|
889
|
+
let dayPeriod;
|
|
890
|
+
this.expr.groups.forEach((group, groupIndex) => {
|
|
891
|
+
if (group.type === undefined) {
|
|
892
|
+
// Skip text (separators, space etc)
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
const match = matches[groupIndex + 1];
|
|
896
|
+
if (getTokenDigits(group) >= 0) {
|
|
897
|
+
// Parse digits
|
|
898
|
+
// This is lenient parsing right now.
|
|
899
|
+
// We should throw exception for strict parsing, if digits are not in range.
|
|
900
|
+
// e.g.
|
|
901
|
+
// 'K' should validate against 0 <= cal.hour && cal.hour <= 11
|
|
902
|
+
// 'h' should validate against 1 <= cal.hour && cal.hour <= 12
|
|
903
|
+
switch (group.type) {
|
|
904
|
+
case 'y': // Year
|
|
905
|
+
cal.year = parseDigits(match, this.digits);
|
|
906
|
+
break;
|
|
907
|
+
case 'M': // Month
|
|
908
|
+
cal.month = parseDigits(match, this.digits);
|
|
909
|
+
break;
|
|
910
|
+
case 'd': // DayOfMonth
|
|
911
|
+
cal.day = parseDigits(match, this.digits);
|
|
912
|
+
break;
|
|
913
|
+
case 'K': // Hour0011
|
|
914
|
+
cal.hour = parseDigits(match, this.digits);
|
|
915
|
+
break;
|
|
916
|
+
case 'H': // Hour0023
|
|
917
|
+
cal.hour = parseDigits(match, this.digits);
|
|
918
|
+
break;
|
|
919
|
+
case 'h': // Hour0112
|
|
920
|
+
cal.hour = parseDigits(match, this.digits);
|
|
921
|
+
break;
|
|
922
|
+
case 'k': // Hour0124
|
|
923
|
+
cal.hour = parseDigits(match, this.digits);
|
|
924
|
+
break;
|
|
925
|
+
case 'm': // Minute
|
|
926
|
+
cal.minute = parseDigits(match, this.digits);
|
|
927
|
+
break;
|
|
928
|
+
case 's': // Second
|
|
929
|
+
cal.second = parseDigits(match, this.digits);
|
|
930
|
+
break;
|
|
931
|
+
case 'A': // MilliSecond
|
|
932
|
+
cal.millisecond = parseDigits(match, this.digits);
|
|
933
|
+
break;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
else {
|
|
937
|
+
// Parse symbols
|
|
938
|
+
const key = getKeyFromSymbol(match, group, this.calendarData);
|
|
939
|
+
if (key) {
|
|
940
|
+
switch (group.type) {
|
|
941
|
+
case 'G': // Era
|
|
942
|
+
era = parseInt(key, 10);
|
|
943
|
+
break;
|
|
944
|
+
case 'M': // Month
|
|
945
|
+
case 'L': // MonthStandalone
|
|
946
|
+
cal.month = MONTHS_KEYS.indexOf(key) + 1; // Calendar month is one indexed
|
|
947
|
+
break;
|
|
948
|
+
case 'a': // PeriodAmPm
|
|
949
|
+
case 'b': // PeriodAmPmNoonMidnight
|
|
950
|
+
case 'B': // PeriodFlexible
|
|
951
|
+
dayPeriod = key;
|
|
952
|
+
break;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
});
|
|
957
|
+
// Adjust year
|
|
958
|
+
if (era !== undefined && era >= 0) {
|
|
959
|
+
const adjustedYear = getGregorianYear(era, cal.year, cal.month, cal.day, this.calendarType, this.commonCalendarData);
|
|
960
|
+
if (adjustedYear !== undefined)
|
|
961
|
+
cal.year = adjustedYear;
|
|
962
|
+
}
|
|
963
|
+
// Adjust hours based period
|
|
964
|
+
if (dayPeriod !== undefined) {
|
|
965
|
+
switch (dayPeriod) {
|
|
966
|
+
case 'am':
|
|
967
|
+
case 'morning1':
|
|
968
|
+
case 'morning2':
|
|
969
|
+
// NOOP
|
|
970
|
+
break;
|
|
971
|
+
case 'pm':
|
|
972
|
+
case 'afternoon1':
|
|
973
|
+
case 'afternoon2':
|
|
974
|
+
case 'evening1':
|
|
975
|
+
case 'night1':
|
|
976
|
+
if (cal.hour >= 0 && cal.hour < 12)
|
|
977
|
+
cal.hour += 12;
|
|
978
|
+
break;
|
|
979
|
+
case 'midnight':
|
|
980
|
+
if (cal.hour === 12)
|
|
981
|
+
cal.hour = 0;
|
|
982
|
+
break;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
return cal.getDate();
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const cldrParserCache = new Cache();
|
|
990
|
+
/**
|
|
991
|
+
* Clears the cache
|
|
992
|
+
*/
|
|
993
|
+
function clearDateTimeCLDRParserCache() {
|
|
994
|
+
cldrParserCache.clear();
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Instantiate or returns a cached value of a CLDR-based date time parser, instantiated with given a parser options.
|
|
998
|
+
*
|
|
999
|
+
* @param options {DateTimeParseOptions} Parser options
|
|
1000
|
+
* @returns {DateTimeParse} Parser instance
|
|
1001
|
+
*/
|
|
1002
|
+
function getDateTimeCLDRParser(options) {
|
|
1003
|
+
if (!cldrParserCache.has(options)) {
|
|
1004
|
+
cldrParserCache.set(options, new DateTimeParseImpl(options));
|
|
1005
|
+
}
|
|
1006
|
+
return cldrParserCache.get(options);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
const intlRelativeTimeFormatCache = new Map();
|
|
1010
|
+
/**
|
|
1011
|
+
* Clears the cache
|
|
1012
|
+
*/
|
|
1013
|
+
function clearRelativeTimeFormatCache() {
|
|
1014
|
+
intlRelativeTimeFormatCache.clear();
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Instantiate or returns a cached value of {@see Intl.RelativeTimeFormat}, instantiated with given options.
|
|
1018
|
+
*
|
|
1019
|
+
* @param options {Intl.RelativeTimeFormatOptions} Formatter options
|
|
1020
|
+
* @returns {Intl.RelativeTimeFormat} Formatter instance
|
|
1021
|
+
*/
|
|
1022
|
+
function getRelativeTimeFormat(locale = 'en-US', options = {}) {
|
|
1023
|
+
if (!intlRelativeTimeFormatCache.has(locale)) {
|
|
1024
|
+
intlRelativeTimeFormatCache.set(locale, new Cache());
|
|
1025
|
+
}
|
|
1026
|
+
if (!intlRelativeTimeFormatCache.get(locale).has(options)) {
|
|
1027
|
+
intlRelativeTimeFormatCache
|
|
1028
|
+
.get(locale)
|
|
1029
|
+
.set(options, new Intl.RelativeTimeFormat(locale, options));
|
|
1030
|
+
}
|
|
1031
|
+
return intlRelativeTimeFormatCache.get(locale).get(options);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
const numberFormatCache = new Map();
|
|
1035
|
+
/**
|
|
1036
|
+
* Clears the cache
|
|
1037
|
+
*/
|
|
1038
|
+
function clearNumberFormatCache() {
|
|
1039
|
+
numberFormatCache.clear();
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Instantiate or returns a cached value of {@see Intl.NumberFormat}, instantiated with given options.
|
|
1043
|
+
*
|
|
1044
|
+
* @param locale {string} Locale
|
|
1045
|
+
* @param options {Intl.NumberFornatOptions} Formatter options
|
|
1046
|
+
* @returns {Intl.NumberFormat} Formatter instance
|
|
1047
|
+
*/
|
|
1048
|
+
function getNumberFormat(locale = 'en-US', options = {}) {
|
|
1049
|
+
if (!numberFormatCache.has(locale)) {
|
|
1050
|
+
numberFormatCache.set(locale, new Cache());
|
|
1051
|
+
}
|
|
1052
|
+
if (!numberFormatCache.get(locale).has(options)) {
|
|
1053
|
+
numberFormatCache.get(locale).set(options, new Intl.NumberFormat(locale, options));
|
|
1054
|
+
}
|
|
1055
|
+
return numberFormatCache.get(locale).get(options);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* Calls getExpression and returns either only a positive expression, or a positive and negative expression if the pattern specifies different patterns for negative values
|
|
1060
|
+
*
|
|
1061
|
+
* @param pattern {string} Number pattern as used on CLDR
|
|
1062
|
+
* @param isNegative {boolean} If this pattern is for negative value. Defaults to false
|
|
1063
|
+
* @param digits {string} String of 10 characters used as digits. e.g. "0123456789" for latm numbering system
|
|
1064
|
+
* @param symbols {LocaleDataNumberSymbol} Symbols used in number pattern. See LocaleDataNumberSymbol
|
|
1065
|
+
* @param currencySymbol {string} Currency symbol used. e.g. "$"
|
|
1066
|
+
* @returns {NumberParseExpressions} A map value of positive and/or negative expression
|
|
1067
|
+
*/
|
|
1068
|
+
function getExpressions(pattern, digits, symbols, currencySymbol, lenient) {
|
|
1069
|
+
const patterns = pattern.split(';');
|
|
1070
|
+
if (patterns.length >= 2) {
|
|
1071
|
+
return {
|
|
1072
|
+
positive: getExpression(patterns[0], false, digits, symbols, currencySymbol, lenient),
|
|
1073
|
+
negative: getExpression(patterns[1], true, digits, symbols, currencySymbol, lenient),
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
return {
|
|
1077
|
+
positive: getExpression(pattern, false, digits, symbols, currencySymbol, lenient),
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Given a pattern and locale data, generates the regular expression and its matching group definitions, to be used when parsing a number string value.
|
|
1082
|
+
*
|
|
1083
|
+
* @param pattern {string} Number pattern as used on CLDR
|
|
1084
|
+
* @param isNegative {boolean} If this pattern is for negative value. Defaults to false
|
|
1085
|
+
* @param digits {string} String of 10 characters used as digits. e.g. "0123456789" for latm numbering system
|
|
1086
|
+
* @param symbols {LocaleDataNumberSymbol} Symbols used in number pattern. See LocaleDataNumberSymbol
|
|
1087
|
+
* @param currencySymbol {string} Currency symbol used. e.g. "$"
|
|
1088
|
+
* @returns {NumberParseExpression} Generate expression and its matching group definition for parsing given number pattern
|
|
1089
|
+
*/
|
|
1090
|
+
function getExpression(pattern, isNegative = false, digits, symbols, currencySymbol, lenient) {
|
|
1091
|
+
const groups = [];
|
|
1092
|
+
// Parse pattern
|
|
1093
|
+
let fractionalZeros = 0; // TODO: Used when min/max digits is implemented
|
|
1094
|
+
let integerZeros = 0; // TODO: Used when min/max digits is implemented
|
|
1095
|
+
// const exponentialDigits = 0; // TODO: support exponential
|
|
1096
|
+
// const exponentialLeadingZeros = 0; // TODO: support exponential
|
|
1097
|
+
const integerSeparatorIntervals = []; // Normal: [3], lakh/crore: [3,2]
|
|
1098
|
+
// let fractionalSeparatorRepeat = -1; // Normal: 0 // NOTE: This is used for parsing
|
|
1099
|
+
const exponentialPosition = pattern.indexOf('E');
|
|
1100
|
+
// TODO: parse exponent
|
|
1101
|
+
const decimalPattern = exponentialPosition > 0 ? pattern.slice(0, exponentialPosition) : pattern.slice(0);
|
|
1102
|
+
const decimalPosition = decimalPattern.indexOf('.');
|
|
1103
|
+
const integerPattern = decimalPosition >= 0 ? decimalPattern.slice(0, decimalPosition) : decimalPattern.slice(0);
|
|
1104
|
+
let integerSeparatorInterval = 0;
|
|
1105
|
+
let integerDigitsStarted = false;
|
|
1106
|
+
let hasPlusSign = false;
|
|
1107
|
+
let hasMinusSign = false;
|
|
1108
|
+
// Read right to left
|
|
1109
|
+
const integerPatternChars = [];
|
|
1110
|
+
[...integerPattern].forEach((char) => integerPatternChars.push(char));
|
|
1111
|
+
integerPatternChars.reverse();
|
|
1112
|
+
[...integerPatternChars].forEach((char) => {
|
|
1113
|
+
switch (char) {
|
|
1114
|
+
case '.':
|
|
1115
|
+
// Ignore decimal sign
|
|
1116
|
+
break;
|
|
1117
|
+
case '+':
|
|
1118
|
+
hasPlusSign = true;
|
|
1119
|
+
groups.push({ token: 'plusSign' });
|
|
1120
|
+
break;
|
|
1121
|
+
case '-':
|
|
1122
|
+
hasMinusSign = true;
|
|
1123
|
+
groups.push({ token: 'minusSign' });
|
|
1124
|
+
break;
|
|
1125
|
+
case '%':
|
|
1126
|
+
groups.push({ token: 'percentSign' });
|
|
1127
|
+
break;
|
|
1128
|
+
case '¤':
|
|
1129
|
+
groups.push({ token: 'currencySign' });
|
|
1130
|
+
break;
|
|
1131
|
+
case '0':
|
|
1132
|
+
integerZeros++;
|
|
1133
|
+
// Continue as digits (#)
|
|
1134
|
+
// eslint-disable-next-line no-fallthrough
|
|
1135
|
+
case '#':
|
|
1136
|
+
// this.integerDigits++;
|
|
1137
|
+
integerSeparatorInterval++;
|
|
1138
|
+
if (!integerDigitsStarted) {
|
|
1139
|
+
groups.push({ token: 'integer' });
|
|
1140
|
+
integerDigitsStarted = true;
|
|
1141
|
+
}
|
|
1142
|
+
break;
|
|
1143
|
+
case ',':
|
|
1144
|
+
integerSeparatorIntervals.push(integerSeparatorInterval);
|
|
1145
|
+
// integerSeparatorRepeat = integerSeparatorIntervals.length - 1; // repeat the last interval
|
|
1146
|
+
integerSeparatorInterval = 0;
|
|
1147
|
+
break;
|
|
1148
|
+
default:
|
|
1149
|
+
groups.push({ token: 'string', value: char });
|
|
1150
|
+
}
|
|
1151
|
+
});
|
|
1152
|
+
groups.reverse();
|
|
1153
|
+
if (decimalPosition >= 0) {
|
|
1154
|
+
groups.push({ token: 'decimalSign' });
|
|
1155
|
+
const fractionalPattern = decimalPattern.slice(decimalPosition);
|
|
1156
|
+
let fractionalDigitsStarted = false;
|
|
1157
|
+
// Read left to right
|
|
1158
|
+
[...fractionalPattern].forEach((char) => {
|
|
1159
|
+
switch (char) {
|
|
1160
|
+
case '.':
|
|
1161
|
+
// Ignore decimal sign
|
|
1162
|
+
break;
|
|
1163
|
+
case '+':
|
|
1164
|
+
hasPlusSign = true;
|
|
1165
|
+
groups.push({ token: 'plusSign' });
|
|
1166
|
+
break;
|
|
1167
|
+
case '-':
|
|
1168
|
+
hasMinusSign = true;
|
|
1169
|
+
groups.push({ token: 'minusSign' });
|
|
1170
|
+
break;
|
|
1171
|
+
case '%':
|
|
1172
|
+
groups.push({ token: 'percentSign' });
|
|
1173
|
+
break;
|
|
1174
|
+
case '¤':
|
|
1175
|
+
groups.push({ token: 'currencySign' });
|
|
1176
|
+
break;
|
|
1177
|
+
case '0':
|
|
1178
|
+
fractionalZeros++;
|
|
1179
|
+
// Continue as digits (#)
|
|
1180
|
+
// eslint-disable-next-line no-fallthrough
|
|
1181
|
+
case '#':
|
|
1182
|
+
if (!fractionalDigitsStarted) {
|
|
1183
|
+
groups.push({ token: 'fraction' });
|
|
1184
|
+
fractionalDigitsStarted = true;
|
|
1185
|
+
}
|
|
1186
|
+
break;
|
|
1187
|
+
case ',':
|
|
1188
|
+
break;
|
|
1189
|
+
default:
|
|
1190
|
+
groups.push({ token: 'string', value: char });
|
|
1191
|
+
}
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
// Generate expression
|
|
1195
|
+
let exprValue = '';
|
|
1196
|
+
for (let i = 0; i < groups.length; i++) {
|
|
1197
|
+
const group = groups[i];
|
|
1198
|
+
switch (group.token) {
|
|
1199
|
+
case 'integer':
|
|
1200
|
+
// Integer part must always has at least one digit (The least is 0)
|
|
1201
|
+
exprValue +=
|
|
1202
|
+
'(' +
|
|
1203
|
+
getDigitsRegexPattern(digits, integerSeparatorIntervals.length > 0 ? symbols.group : '') +
|
|
1204
|
+
'+)';
|
|
1205
|
+
break;
|
|
1206
|
+
case 'fraction':
|
|
1207
|
+
// Fractional part is optional if lenient, or min fractional digits is 0
|
|
1208
|
+
exprValue +=
|
|
1209
|
+
'(' +
|
|
1210
|
+
getDigitsRegexPattern(digits, integerSeparatorIntervals.length > 0 ? symbols.group : '') +
|
|
1211
|
+
'+)';
|
|
1212
|
+
if (lenient || (decimalPosition > 0 && fractionalZeros <= 0)) {
|
|
1213
|
+
exprValue += '?';
|
|
1214
|
+
}
|
|
1215
|
+
break;
|
|
1216
|
+
case 'decimalSign':
|
|
1217
|
+
// Decimal sign is optional if lenient, or if min fractional digits is 0 (fractional is optional)
|
|
1218
|
+
exprValue += '(' + escapeRegex(symbols.decimal) + ')';
|
|
1219
|
+
if (lenient || (decimalPosition > 0 && fractionalZeros <= 0)) {
|
|
1220
|
+
exprValue += '?';
|
|
1221
|
+
}
|
|
1222
|
+
break;
|
|
1223
|
+
case 'plusSign':
|
|
1224
|
+
// Optional
|
|
1225
|
+
exprValue += '(' + escapeRegex(symbols.plusSign) + ')?';
|
|
1226
|
+
break;
|
|
1227
|
+
case 'minusSign':
|
|
1228
|
+
// Optional
|
|
1229
|
+
exprValue += '(' + escapeRegex(symbols.minusSign) + ')?';
|
|
1230
|
+
break;
|
|
1231
|
+
case 'percentSign':
|
|
1232
|
+
// Optional
|
|
1233
|
+
exprValue += '(' + escapeRegex(symbols.percentSign) + ')?';
|
|
1234
|
+
break;
|
|
1235
|
+
case 'currencySign':
|
|
1236
|
+
// Optional
|
|
1237
|
+
exprValue += '(' + escapeRegex(currencySymbol) + ')?';
|
|
1238
|
+
break;
|
|
1239
|
+
case 'string':
|
|
1240
|
+
if (group.value !== undefined) {
|
|
1241
|
+
// Group to \s if value is all whitespace
|
|
1242
|
+
if (/^\s+$/.test(group.value)) {
|
|
1243
|
+
exprValue += '(\\s+)' + (lenient ? '?' : '');
|
|
1244
|
+
}
|
|
1245
|
+
else {
|
|
1246
|
+
exprValue += '(' + escapeRegex(group.value) + ')';
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
break;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
// Prepend optional plus/minus sign if not available in pattern
|
|
1253
|
+
if (!isNegative) {
|
|
1254
|
+
if (!hasPlusSign && !hasMinusSign) {
|
|
1255
|
+
exprValue =
|
|
1256
|
+
'(' +
|
|
1257
|
+
escapeRegex(symbols.plusSign) +
|
|
1258
|
+
'|' +
|
|
1259
|
+
escapeRegex(symbols.minusSign) +
|
|
1260
|
+
')?' +
|
|
1261
|
+
exprValue;
|
|
1262
|
+
groups.unshift({ token: 'plusOrMinusSign' });
|
|
1263
|
+
}
|
|
1264
|
+
else if (!hasPlusSign) {
|
|
1265
|
+
exprValue = '(' + escapeRegex(symbols.plusSign) + ')?' + exprValue;
|
|
1266
|
+
groups.unshift({ token: 'plusSign' });
|
|
1267
|
+
}
|
|
1268
|
+
else if (!hasMinusSign) {
|
|
1269
|
+
exprValue = '(' + escapeRegex(symbols.minusSign) + ')?' + exprValue;
|
|
1270
|
+
groups.unshift({ token: 'minusSign' });
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
const expr = new RegExp(exprValue);
|
|
1274
|
+
return {
|
|
1275
|
+
expr,
|
|
1276
|
+
groups,
|
|
1277
|
+
isNegative,
|
|
1278
|
+
minIntegerDigits: integerZeros,
|
|
1279
|
+
minFractionalDigits: fractionalZeros,
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
*
|
|
1284
|
+
* @param value {string} String value to parse
|
|
1285
|
+
* @param useNegativeExpr {boolean} Use negativeExpr to parse this value instead of the default positiveExpr
|
|
1286
|
+
* @param positiveExpr {NumberParseExpression} Expression to use to parse this value for positive values
|
|
1287
|
+
* @param negativeExpr {NumberParseExpression} Expression to use to parse this value for negative values
|
|
1288
|
+
* @param digits {string} String of 10 characters used as digits. e.g. "0123456789" for latm numbering system
|
|
1289
|
+
* @param symbols {LocaleDataNumberSymbol} Symbols used in number pattern
|
|
1290
|
+
*/
|
|
1291
|
+
function parseNumber(value, useNegativeExpr = false, positiveExpr, negativeExpr, digits, symbols) {
|
|
1292
|
+
// Parse with negative expression first if available and return if value is valid
|
|
1293
|
+
if (!useNegativeExpr && negativeExpr) {
|
|
1294
|
+
const result = parseNumber(value, true, positiveExpr, negativeExpr, digits, symbols);
|
|
1295
|
+
if (!isNaN(result))
|
|
1296
|
+
return result;
|
|
1297
|
+
}
|
|
1298
|
+
// Parse using provided negative expression if requested
|
|
1299
|
+
const expr = useNegativeExpr && negativeExpr ? negativeExpr : positiveExpr;
|
|
1300
|
+
const matches = value.match(expr.expr);
|
|
1301
|
+
if (!matches) {
|
|
1302
|
+
return NaN;
|
|
1303
|
+
}
|
|
1304
|
+
let hasMinusSign = false;
|
|
1305
|
+
let hasPlusSign = false;
|
|
1306
|
+
let isPercentPattern = false;
|
|
1307
|
+
let integerPart = '';
|
|
1308
|
+
let fractionPart = '';
|
|
1309
|
+
const groupRegex = new RegExp(escapeRegex(symbols.group), 'g');
|
|
1310
|
+
for (let i = 0; i < expr.groups.length; i++) {
|
|
1311
|
+
const group = expr.groups[i];
|
|
1312
|
+
isPercentPattern = isPercentPattern || group.token === 'percentSign';
|
|
1313
|
+
const match = matches[i + 1];
|
|
1314
|
+
if (!match)
|
|
1315
|
+
continue;
|
|
1316
|
+
switch (group.token) {
|
|
1317
|
+
case 'integer':
|
|
1318
|
+
integerPart += parseDigits(match.replace(groupRegex, ''), digits);
|
|
1319
|
+
break;
|
|
1320
|
+
case 'fraction':
|
|
1321
|
+
fractionPart += parseDigits(match.replace(groupRegex, ''), digits);
|
|
1322
|
+
break;
|
|
1323
|
+
case 'plusSign':
|
|
1324
|
+
hasPlusSign = true;
|
|
1325
|
+
break;
|
|
1326
|
+
case 'minusSign':
|
|
1327
|
+
hasMinusSign = true;
|
|
1328
|
+
break;
|
|
1329
|
+
case 'plusOrMinusSign':
|
|
1330
|
+
switch (match) {
|
|
1331
|
+
case symbols.plusSign:
|
|
1332
|
+
hasPlusSign = true;
|
|
1333
|
+
break;
|
|
1334
|
+
case symbols.minusSign:
|
|
1335
|
+
hasMinusSign = true;
|
|
1336
|
+
break;
|
|
1337
|
+
}
|
|
1338
|
+
break;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
// Build number
|
|
1342
|
+
if (hasMinusSign && hasPlusSign) {
|
|
1343
|
+
throw new Error('String has both plus and minus sign');
|
|
1344
|
+
}
|
|
1345
|
+
let result = NaN;
|
|
1346
|
+
// Remove leading zeros on integer
|
|
1347
|
+
integerPart.replace(/\b0+\B/, '');
|
|
1348
|
+
if (isPercentPattern) {
|
|
1349
|
+
// Shift 2 digits to the right (divide by 100) if pattern contains percent sign
|
|
1350
|
+
fractionPart = integerPart.slice(-2).padStart(2, '0') + fractionPart;
|
|
1351
|
+
integerPart = integerPart.slice(0, -2).padStart(1, '0');
|
|
1352
|
+
let num = (hasMinusSign ? '-' : '') + integerPart;
|
|
1353
|
+
if (fractionPart !== '') {
|
|
1354
|
+
num += '.' + fractionPart;
|
|
1355
|
+
}
|
|
1356
|
+
result = Number.parseFloat(num);
|
|
1357
|
+
}
|
|
1358
|
+
else {
|
|
1359
|
+
let num = (hasMinusSign ? '-' : '') + integerPart;
|
|
1360
|
+
if (expr.minFractionalDigits === undefined ||
|
|
1361
|
+
(expr.minFractionalDigits !== undefined &&
|
|
1362
|
+
expr.minFractionalDigits <= 0 &&
|
|
1363
|
+
fractionPart === '')) {
|
|
1364
|
+
// Treat as integer if fraction is optional
|
|
1365
|
+
result = Number.parseInt(num);
|
|
1366
|
+
}
|
|
1367
|
+
else {
|
|
1368
|
+
if (fractionPart !== '') {
|
|
1369
|
+
num += '.' + fractionPart;
|
|
1370
|
+
}
|
|
1371
|
+
result = Number.parseFloat(num);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
// Negate result if negative expression is used
|
|
1375
|
+
return useNegativeExpr && negativeExpr ? -result : result;
|
|
1376
|
+
}
|
|
1377
|
+
/**
|
|
1378
|
+
* Number parser implementation
|
|
1379
|
+
*
|
|
1380
|
+
* @author aazizy
|
|
1381
|
+
*/
|
|
1382
|
+
class NumberParseImpl {
|
|
1383
|
+
/**
|
|
1384
|
+
* @constructor
|
|
1385
|
+
* @param options {NumberParseOptions} Options used for this parser instance
|
|
1386
|
+
*/
|
|
1387
|
+
constructor(options) {
|
|
1388
|
+
// Build data
|
|
1389
|
+
const data = {
|
|
1390
|
+
commonDigits,
|
|
1391
|
+
defaultNumberingSystem,
|
|
1392
|
+
currencySymbol,
|
|
1393
|
+
symbols: {
|
|
1394
|
+
decimal: decimalSeparator,
|
|
1395
|
+
group: groupingSeparator,
|
|
1396
|
+
percentSign: percentSign,
|
|
1397
|
+
plusSign: plusSign,
|
|
1398
|
+
minusSign: minusSign,
|
|
1399
|
+
exponential: exponentialSign,
|
|
1400
|
+
superScriptingExponent: superscriptingExponentSign,
|
|
1401
|
+
perMille: perMilleSign,
|
|
1402
|
+
infinity: infinity,
|
|
1403
|
+
nan: nan,
|
|
1404
|
+
},
|
|
1405
|
+
};
|
|
1406
|
+
// Resolve options
|
|
1407
|
+
options.numberingSystem = options.numberingSystem
|
|
1408
|
+
? options.numberingSystem
|
|
1409
|
+
: data.defaultNumberingSystem;
|
|
1410
|
+
this.options = options;
|
|
1411
|
+
this.digits = data.commonDigits[options.numberingSystem];
|
|
1412
|
+
this.symbols = data.symbols;
|
|
1413
|
+
this.currencySymbol = data.currencySymbol;
|
|
1414
|
+
this.lenient = !(options.lenient === false); // Default is true
|
|
1415
|
+
// Generating expression here becuse we want to do this only once.
|
|
1416
|
+
const exprs = getExpressions(this.options.pattern, this.digits, this.symbols, this.currencySymbol, this.lenient);
|
|
1417
|
+
this.positiveExpr = exprs.positive;
|
|
1418
|
+
this.negativeExpr = exprs.negative;
|
|
1419
|
+
}
|
|
1420
|
+
/**
|
|
1421
|
+
* @inheritdoc
|
|
1422
|
+
*/
|
|
1423
|
+
parse(value) {
|
|
1424
|
+
return parseNumber(value, true, this.positiveExpr, this.negativeExpr, this.digits, this.symbols);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
const NumberParseCache = new Cache();
|
|
1429
|
+
/**
|
|
1430
|
+
* Clears the cache
|
|
1431
|
+
*/
|
|
1432
|
+
function clearNumberParserCache() {
|
|
1433
|
+
NumberParseCache.clear();
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* Instantiate or returns a cached value of {@see NumberParse}, instantiated with given options.
|
|
1437
|
+
*
|
|
1438
|
+
* @param options {NumberParseOptions} Parser options
|
|
1439
|
+
* @returns {NumberParse} Parser instance
|
|
1440
|
+
*/
|
|
1441
|
+
function getNumberParser(options) {
|
|
1442
|
+
if (!NumberParseCache.has(options)) {
|
|
1443
|
+
NumberParseCache.set(options, new NumberParseImpl(options));
|
|
1444
|
+
}
|
|
1445
|
+
return NumberParseCache.get(options);
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
/**
|
|
1449
|
+
* Regular expression for parsing ISO-8601 datetime string.
|
|
1450
|
+
*
|
|
1451
|
+
* RegExp is thread safe so we can reuse this to match across instances.
|
|
1452
|
+
*
|
|
1453
|
+
* Please group indices accordingly if expression is updated.
|
|
1454
|
+
*
|
|
1455
|
+
* Ref https://en.wikipedia.org/wiki/ISO_8601
|
|
1456
|
+
*/
|
|
1457
|
+
const EXPR = /^[\s]*(((-|\+)?([0-9]{4})[-]?|--)(([0-9]{2})|([0-9]{2})[-]?([0-9]{2})|(W([0-9]{2})([-]?([0-9]))?)|[-]?([0-9]{3})))(T([0-9]{2})([:]?([0-9]{2})([:]?([0-9]{2})(\.([0-9]{3}))?)?)?(Z|(-|\+)([0-9]{2})([:]?([0-9]{2})?)?)?)?[\s]*$/;
|
|
1458
|
+
/** (full string) ((-|+)yyyy-|--)(MM|MM-dd|Www-E|DDD)'T'hh:mm:ss.SSS('Z'|(-|+)xx:yy) */
|
|
1459
|
+
/** (date part) ((-|+)yyyy-|--)(MM|MM-dd|Www-E|DDD) */
|
|
1460
|
+
const INDEX_DATE = 1;
|
|
1461
|
+
/** (year part) (-|+)yyyy-|--) */
|
|
1462
|
+
const INDEX_YEAR_OR_NONE = INDEX_DATE + 1;
|
|
1463
|
+
/** (-|+)? */
|
|
1464
|
+
const INDEX_YEAR_SIGN = INDEX_YEAR_OR_NONE + 1;
|
|
1465
|
+
/** yyyy */
|
|
1466
|
+
const INDEX_YEAR = INDEX_YEAR_OR_NONE + 2;
|
|
1467
|
+
/** (MM|MM-dd|Www-E|MM-dd) */
|
|
1468
|
+
const INDEX_MONTH_MONTHDATE_WEEK_ORDINAL = INDEX_YEAR + 1;
|
|
1469
|
+
/** MM */
|
|
1470
|
+
const INDEX_MONTH = INDEX_MONTH_MONTHDATE_WEEK_ORDINAL + 1;
|
|
1471
|
+
/** MM-dd */
|
|
1472
|
+
const INDEX_MONTHDATE = INDEX_MONTH_MONTHDATE_WEEK_ORDINAL + 2;
|
|
1473
|
+
/** (week part) Www-E */
|
|
1474
|
+
const INDEX_WEEK = INDEX_MONTH_MONTHDATE_WEEK_ORDINAL + 4;
|
|
1475
|
+
/** (ordinal) DDD */
|
|
1476
|
+
const INDEX_ORDINAL = INDEX_MONTH_MONTHDATE_WEEK_ORDINAL + 8;
|
|
1477
|
+
/** (time part) 'T'hh:mm:ss.SSS('Z'|(-|+)xx:yy) */
|
|
1478
|
+
const INDEX_TIME = INDEX_MONTH_MONTHDATE_WEEK_ORDINAL + 9;
|
|
1479
|
+
/** :mm:ss.SSS */
|
|
1480
|
+
const INDEX_MINUTE = INDEX_TIME + 2;
|
|
1481
|
+
/** :ss.SSS */
|
|
1482
|
+
const INDEX_SECOND = INDEX_TIME + 4;
|
|
1483
|
+
/** .SSS, (millisecond must be prefixed by a period, even on abbreviated version) */
|
|
1484
|
+
const INDEX_MILLISECOND = INDEX_TIME + 6;
|
|
1485
|
+
/** (timezone part) 'Z'|(-|+)xx:yy */
|
|
1486
|
+
const INDEX_TZ = INDEX_TIME + 8;
|
|
1487
|
+
/** :yy */
|
|
1488
|
+
const INDEX_TZ_MINUTE = INDEX_TIME + 11;
|
|
1489
|
+
const TIME_ONLY_EXPR = /^[\s]*(([0-9]{2})([:]?([0-9]{2})([:]?([0-9]{2})(\.([0-9]{3}))?)?)?(Z|(-|\+)([0-9]{2})([:]?([0-9]{2})?)?)?)[\s]*$/;
|
|
1490
|
+
const TIME_ONLY_INDEX_TIME = 1;
|
|
1491
|
+
const TIME_ONLY_INDEX_MINUTE = TIME_ONLY_INDEX_TIME + 2;
|
|
1492
|
+
const TIME_ONLY_INDEX_SECOND = TIME_ONLY_INDEX_TIME + 4;
|
|
1493
|
+
const TIME_ONLY_INDEX_MILLISECOND = TIME_ONLY_INDEX_TIME + 6;
|
|
1494
|
+
const TIME_ONLY_INDEX_TZ = TIME_ONLY_INDEX_TIME + 8;
|
|
1495
|
+
const TIME_ONLY_INDEX_TZ_MINUTE = TIME_ONLY_INDEX_TIME + 11;
|
|
1496
|
+
/**
|
|
1497
|
+
* Parse an ISO-8601 datetime string representation to a Date object, or throw on unparseable value.
|
|
1498
|
+
*
|
|
1499
|
+
* This parser adheres to ISO-8601:2019, where the time separator 'T' is required, and full date part is required, and minute/second/millisecond is optional.
|
|
1500
|
+
*
|
|
1501
|
+
* Time defaults to 00:00:00.000Z if not specified.
|
|
1502
|
+
*
|
|
1503
|
+
* Timezone defaults to UTC if not specified.
|
|
1504
|
+
*
|
|
1505
|
+
* This parser tries to comply with ISO 8601:2004, which implies not supporting some of previously defined representations, such as YYMMDD, --MMDD
|
|
1506
|
+
*
|
|
1507
|
+
* Provides a more consistent cross-platform parsing for ISO-8601 (only) date strings
|
|
1508
|
+
* but behavior is different on each implementation and not recommended, as noted here:
|
|
1509
|
+
*
|
|
1510
|
+
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date#Timestamp_string
|
|
1511
|
+
*
|
|
1512
|
+
* @param value {string} Value to parse
|
|
1513
|
+
* @returns {Date} Date instance
|
|
1514
|
+
*/
|
|
1515
|
+
function parseDateTimeIsoString(value) {
|
|
1516
|
+
if (value && EXPR.test(value)) {
|
|
1517
|
+
return parseDateTimeString(value);
|
|
1518
|
+
}
|
|
1519
|
+
else if (value && TIME_ONLY_EXPR.test(value)) {
|
|
1520
|
+
return parseTimeOnlyString(value);
|
|
1521
|
+
}
|
|
1522
|
+
throw new Error("Unparseable date '" + value + "'");
|
|
1523
|
+
}
|
|
1524
|
+
function parseTimeOnlyString(value) {
|
|
1525
|
+
const matches = value.match(TIME_ONLY_EXPR);
|
|
1526
|
+
if (matches) {
|
|
1527
|
+
// Time parts
|
|
1528
|
+
let hour = 0;
|
|
1529
|
+
let minute = 0;
|
|
1530
|
+
let second = 0;
|
|
1531
|
+
let millisecond = 0;
|
|
1532
|
+
if (matches[TIME_ONLY_INDEX_TIME] !== undefined) {
|
|
1533
|
+
hour = Number(matches[TIME_ONLY_INDEX_TIME + 1]); // hh : hour part is required
|
|
1534
|
+
if (matches[TIME_ONLY_INDEX_MINUTE]) {
|
|
1535
|
+
minute = Number(matches[TIME_ONLY_INDEX_MINUTE + 1]); // mm : minute part is optional
|
|
1536
|
+
}
|
|
1537
|
+
if (matches[TIME_ONLY_INDEX_SECOND] !== undefined) {
|
|
1538
|
+
second = Number(matches[TIME_ONLY_INDEX_SECOND + 1]); // ss : second part is optional
|
|
1539
|
+
}
|
|
1540
|
+
if (matches[TIME_ONLY_INDEX_MILLISECOND] !== undefined) {
|
|
1541
|
+
millisecond = Number(matches[TIME_ONLY_INDEX_MILLISECOND + 1]); // SSS : millisecond part is optional
|
|
1542
|
+
// period on millisecond is required on both basic and extended format
|
|
1543
|
+
}
|
|
1544
|
+
// matches[INDEX_TZ] examples: 'Z', '+09:00', '-1100', '+09', '-11'
|
|
1545
|
+
if (matches[TIME_ONLY_INDEX_TZ] !== undefined) {
|
|
1546
|
+
// Only adjust offset if this is not UTC
|
|
1547
|
+
if (matches[TIME_ONLY_INDEX_TZ] !== 'Z') {
|
|
1548
|
+
let offset = Number(matches[TIME_ONLY_INDEX_TZ + 2]) * 60; // offset hour part is required for non 'Z' offset
|
|
1549
|
+
if (matches[TIME_ONLY_INDEX_TZ_MINUTE] !== undefined) {
|
|
1550
|
+
// offset minute part is optional
|
|
1551
|
+
offset += Number(matches[TIME_ONLY_INDEX_TZ_MINUTE + 1]);
|
|
1552
|
+
}
|
|
1553
|
+
if (matches[TIME_ONLY_INDEX_TZ + 1] === '+') {
|
|
1554
|
+
// plus/minus is required for non 'Z' offset
|
|
1555
|
+
// If offset is positive, substract offset from minutes
|
|
1556
|
+
// e.g. 11am in +09:00 is 2am in UTC (11:00 - 09:00)
|
|
1557
|
+
offset = -offset;
|
|
1558
|
+
}
|
|
1559
|
+
minute += offset;
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
const date = new Date();
|
|
1564
|
+
// Date is instantiated with runtime timezone offset.
|
|
1565
|
+
// We use Date.UTC to generate a timestamp and use that to instantiate the Date instance.
|
|
1566
|
+
const timestamp = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), hour, minute, second, millisecond);
|
|
1567
|
+
const parsedDate = new Date(timestamp);
|
|
1568
|
+
return parsedDate;
|
|
1569
|
+
}
|
|
1570
|
+
throw new Error("Unparseable date '" + value + "'");
|
|
1571
|
+
}
|
|
1572
|
+
function parseDateTimeString(value) {
|
|
1573
|
+
const matches = value.match(EXPR);
|
|
1574
|
+
if (matches) {
|
|
1575
|
+
// Date/time parts in UTC
|
|
1576
|
+
/** This will stay undefined for --MM-dd and --MMdd representation */
|
|
1577
|
+
let year;
|
|
1578
|
+
let isLeapYear;
|
|
1579
|
+
/** Only a full date can be combined with time */
|
|
1580
|
+
let isFullDate = true;
|
|
1581
|
+
let isDateExtendedFormat = false;
|
|
1582
|
+
if (matches[INDEX_YEAR]) {
|
|
1583
|
+
year = Number(matches[INDEX_YEAR]); // yyyy : year part
|
|
1584
|
+
if (matches[INDEX_YEAR_SIGN] && matches[INDEX_YEAR_SIGN] === '-') {
|
|
1585
|
+
year = -year;
|
|
1586
|
+
}
|
|
1587
|
+
isDateExtendedFormat =
|
|
1588
|
+
isDateExtendedFormat || matches[INDEX_YEAR_OR_NONE].endsWith('-');
|
|
1589
|
+
}
|
|
1590
|
+
let month = 1; // Defaults to Jan 1
|
|
1591
|
+
let day = 1; // Defaults to Jan 1
|
|
1592
|
+
if (year !== undefined && matches[INDEX_MONTH_MONTHDATE_WEEK_ORDINAL]) {
|
|
1593
|
+
isLeapYear = new Date(Date.UTC(year, 1, 29, 0, 0, 0, 0)).getUTCDate() === 29;
|
|
1594
|
+
isDateExtendedFormat =
|
|
1595
|
+
isDateExtendedFormat ||
|
|
1596
|
+
matches[INDEX_MONTH_MONTHDATE_WEEK_ORDINAL].indexOf('-') >= 0;
|
|
1597
|
+
if (matches[INDEX_MONTH]) {
|
|
1598
|
+
// Only extended form YYYY-MM is allowed, not YYYYMM
|
|
1599
|
+
if (!matches[INDEX_YEAR_OR_NONE].endsWith('-')) {
|
|
1600
|
+
throw new Error("Unparseable date '" + value + "'");
|
|
1601
|
+
}
|
|
1602
|
+
// Calendar date, with date omitted
|
|
1603
|
+
month = Number(matches[INDEX_MONTH]); // MM : month part
|
|
1604
|
+
day = 1;
|
|
1605
|
+
isFullDate = false;
|
|
1606
|
+
}
|
|
1607
|
+
else if (matches[INDEX_MONTHDATE]) {
|
|
1608
|
+
// Calendar date
|
|
1609
|
+
month = Number(matches[INDEX_MONTHDATE]); // MM : month part
|
|
1610
|
+
day = Number(matches[INDEX_MONTHDATE + 1]); // dd : date part
|
|
1611
|
+
}
|
|
1612
|
+
else if (matches[INDEX_WEEK]) {
|
|
1613
|
+
const week = Number(matches[INDEX_WEEK + 1]); // ww : week part
|
|
1614
|
+
let dayOfWeek = 1; // 1: monday, 7: sunday
|
|
1615
|
+
if (matches[INDEX_WEEK + 2]) {
|
|
1616
|
+
dayOfWeek = Number(matches[INDEX_WEEK + 3]); // E : day of week part is optional
|
|
1617
|
+
}
|
|
1618
|
+
else {
|
|
1619
|
+
isFullDate = false;
|
|
1620
|
+
}
|
|
1621
|
+
// Validate week/dayOfWeek
|
|
1622
|
+
if (!isValidWeek(week) || !isValidDayOfWeek(dayOfWeek)) {
|
|
1623
|
+
throw new Error("Unparseable date '" + value + "'");
|
|
1624
|
+
}
|
|
1625
|
+
// Calculate month and date from Www-E
|
|
1626
|
+
const jan1 = new Date(Date.UTC(year, 0, 1, 0, 0, 0, 0));
|
|
1627
|
+
// Date#getDay|getUTCDay returns 0: sunday, 6: saturday
|
|
1628
|
+
const jan1Day = jan1.getUTCDay();
|
|
1629
|
+
// If 1 January is on a Monday, Tuesday, Wednesday or Thursday, it is in week 01.
|
|
1630
|
+
// Else 1 January is on last week of previous year (week 52 or 53)
|
|
1631
|
+
// So, to summarize (jan1Day: W01-1 date) is:
|
|
1632
|
+
// 1: Jan 1
|
|
1633
|
+
// 2: Dec 31
|
|
1634
|
+
// 3: Dec 30
|
|
1635
|
+
// 4: Dec 29
|
|
1636
|
+
// 5: Jan 4
|
|
1637
|
+
// 6: Jan 3
|
|
1638
|
+
// 0: Jan 2
|
|
1639
|
+
// Date constructor allows overflow of date and correctly recalculate the upper parts (month, year)
|
|
1640
|
+
// Source is kept verbose. This is basically doing this:
|
|
1641
|
+
// const weekDate = (jan1Day === 2 || jan1Day === 3 || jan1Day === 4) ? //
|
|
1642
|
+
// new Date(Date.UTC(year - 1, 11, 7 * (week - 1) + (33 - jan1Day) + (dayOfWeek - 1), 0, 0, 0, 0)) : //
|
|
1643
|
+
// new Date(Date.UTC(year, 0, 7 * (week - 1) + ((9 - jan1Day) % 7) + (dayOfWeek - 1), 0, 0, 0, 0));
|
|
1644
|
+
const week1Day1Year = jan1Day === 2 || jan1Day === 3 || jan1Day === 4 ? year - 1 : year;
|
|
1645
|
+
const week1Day1Month = jan1Day === 2 || jan1Day === 3 || jan1Day === 4 ? 12 : 1;
|
|
1646
|
+
const week1Day1Date = jan1Day === 2 || jan1Day === 3 || jan1Day === 4
|
|
1647
|
+
? 33 - jan1Day
|
|
1648
|
+
: (9 - jan1Day) % 7;
|
|
1649
|
+
// Date constructor allows overflow of date and correctly recalculate the upper parts (month, year)
|
|
1650
|
+
const weekDate = new Date(Date.UTC(week1Day1Year, week1Day1Month - 1, week1Day1Date + 7 * (week - 1) + (dayOfWeek - 1), 0, 0, 0, 0));
|
|
1651
|
+
// Override year, month and date
|
|
1652
|
+
year = weekDate.getUTCFullYear();
|
|
1653
|
+
month = weekDate.getUTCMonth() + 1;
|
|
1654
|
+
day = weekDate.getUTCDate();
|
|
1655
|
+
}
|
|
1656
|
+
else if (matches[INDEX_ORDINAL]) {
|
|
1657
|
+
// Ordinal dates
|
|
1658
|
+
// 1: Jan 1, 365|366: Dec 31
|
|
1659
|
+
const ordinal = Number(matches[INDEX_ORDINAL]); // DDD : ordinal day part
|
|
1660
|
+
// Validate ordinal
|
|
1661
|
+
if (!isValidOrdinal(ordinal, isLeapYear)) {
|
|
1662
|
+
throw new Error("Unparseable date '" + value + "'");
|
|
1663
|
+
}
|
|
1664
|
+
// Date constructor allows overflow of date and correctly recalculate the upper parts (month, year)
|
|
1665
|
+
const ordinalDate = new Date(Date.UTC(year, 0, ordinal, 0, 0, 0, 0));
|
|
1666
|
+
// Override month and date
|
|
1667
|
+
month = ordinalDate.getUTCMonth() + 1;
|
|
1668
|
+
day = ordinalDate.getUTCDate();
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
else if (year === undefined &&
|
|
1672
|
+
matches[INDEX_MONTH_MONTHDATE_WEEK_ORDINAL] &&
|
|
1673
|
+
matches[INDEX_MONTHDATE]) {
|
|
1674
|
+
// Only --MM-dd and --MMdd representation do not have year defined.
|
|
1675
|
+
// We support this pattern by using 2000 as year since it is a leap year (to allow --02-29).
|
|
1676
|
+
// We can default lesser significant part (e.g. month, date, hours) and have a sensibly usable value,
|
|
1677
|
+
// but, this representation is not suitable for JS Date since the significant part (year) of a calendar date is missing.
|
|
1678
|
+
// Calendar date
|
|
1679
|
+
year = 2000; // year defaults 2000
|
|
1680
|
+
month = Number(matches[INDEX_MONTHDATE]); // MM : month part
|
|
1681
|
+
day = Number(matches[INDEX_MONTHDATE + 1]); // dd : date part
|
|
1682
|
+
isLeapYear = new Date(Date.UTC(year, 1, 29, 0, 0, 0, 0)).getUTCDate() === 29;
|
|
1683
|
+
isFullDate = false;
|
|
1684
|
+
isDateExtendedFormat =
|
|
1685
|
+
isDateExtendedFormat ||
|
|
1686
|
+
matches[INDEX_MONTH_MONTHDATE_WEEK_ORDINAL].indexOf('-') >= 0;
|
|
1687
|
+
}
|
|
1688
|
+
else {
|
|
1689
|
+
throw new Error("Unparseable date '" + value + "'");
|
|
1690
|
+
}
|
|
1691
|
+
if (!isValidDate(month, day, isLeapYear)) {
|
|
1692
|
+
throw new Error("Unparseable date '" + value + "'");
|
|
1693
|
+
}
|
|
1694
|
+
// Time parts
|
|
1695
|
+
let hour = 0;
|
|
1696
|
+
let minute = 0;
|
|
1697
|
+
let second = 0;
|
|
1698
|
+
let millisecond = 0;
|
|
1699
|
+
let isTimeExtendedFormat = false;
|
|
1700
|
+
let isHourOnly = true;
|
|
1701
|
+
// matches[INDEX_TIME] examples: 'T12:34:56.789+10:00', 'T00:00Z'
|
|
1702
|
+
if (matches[INDEX_TIME] !== undefined) {
|
|
1703
|
+
if (!isFullDate) {
|
|
1704
|
+
throw new Error("Unparseable date '" + value + "'");
|
|
1705
|
+
}
|
|
1706
|
+
hour = Number(matches[INDEX_TIME + 1]); // hh : hour part is required
|
|
1707
|
+
if (matches[INDEX_MINUTE]) {
|
|
1708
|
+
minute = Number(matches[INDEX_MINUTE + 1]); // mm : minute part is optional
|
|
1709
|
+
isTimeExtendedFormat =
|
|
1710
|
+
isTimeExtendedFormat || matches[INDEX_MINUTE].startsWith(':');
|
|
1711
|
+
isHourOnly = false;
|
|
1712
|
+
}
|
|
1713
|
+
if (matches[INDEX_SECOND] !== undefined) {
|
|
1714
|
+
second = Number(matches[INDEX_SECOND + 1]); // ss : second part is optional
|
|
1715
|
+
isTimeExtendedFormat =
|
|
1716
|
+
isTimeExtendedFormat || matches[INDEX_SECOND].startsWith(':');
|
|
1717
|
+
isHourOnly = false;
|
|
1718
|
+
}
|
|
1719
|
+
if (matches[INDEX_MILLISECOND] !== undefined) {
|
|
1720
|
+
millisecond = Number(matches[INDEX_MILLISECOND + 1]); // SSS : millisecond part is optional
|
|
1721
|
+
// period on millisecond is required on both basic and extended format
|
|
1722
|
+
}
|
|
1723
|
+
// matches[INDEX_TZ] examples: 'Z', '+09:00', '-1100', '+09', '-11'
|
|
1724
|
+
if (matches[INDEX_TZ] !== undefined) {
|
|
1725
|
+
// Only adjust offset if this is not UTC
|
|
1726
|
+
if (matches[INDEX_TZ] !== 'Z') {
|
|
1727
|
+
let offset = Number(matches[INDEX_TZ + 2]) * 60; // offset hour part is required for non 'Z' offset
|
|
1728
|
+
if (matches[INDEX_TZ_MINUTE] !== undefined) {
|
|
1729
|
+
// offset minute part is optional
|
|
1730
|
+
offset += Number(matches[INDEX_TZ_MINUTE + 1]);
|
|
1731
|
+
}
|
|
1732
|
+
if (matches[INDEX_TZ + 1] === '+') {
|
|
1733
|
+
// plus/minus is required for non 'Z' offset
|
|
1734
|
+
// If offset is positive, substract offset from minutes
|
|
1735
|
+
// e.g. 11am in +09:00 is 2am in UTC (11:00 - 09:00)
|
|
1736
|
+
offset = -offset;
|
|
1737
|
+
}
|
|
1738
|
+
minute += offset;
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
// Combined date and time must have the same format
|
|
1742
|
+
// Hour only is valid, and the same for both basic and extended format
|
|
1743
|
+
if (isDateExtendedFormat !== isTimeExtendedFormat && !isHourOnly) {
|
|
1744
|
+
throw new Error("Unparseable date '" + value + "'");
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
// Date is instantiated with runtime timezone offset.
|
|
1748
|
+
// We use Date.UTC to generate a timestamp and use that to instantiate the Date instance.
|
|
1749
|
+
const timestamp = Date.UTC(year, month - 1, day, hour, minute, second, millisecond);
|
|
1750
|
+
return new Date(timestamp);
|
|
1751
|
+
}
|
|
1752
|
+
throw new Error("Unparseable date '" + value + "'");
|
|
1753
|
+
}
|
|
1754
|
+
/**
|
|
1755
|
+
* Parse an ISO-8601 datetime string representation to a Date object, or throw on unparseable value.
|
|
1756
|
+
*
|
|
1757
|
+
* This parser adheres to ISO-8601:2019, where the time separator 'T' is required, and full date part is required, and minute/second/millisecond is optional.
|
|
1758
|
+
*
|
|
1759
|
+
* Time defaults to 00:00:00.000Z if not specified.
|
|
1760
|
+
*
|
|
1761
|
+
* Timezone defaults to UTC if not specified.
|
|
1762
|
+
*
|
|
1763
|
+
* Only calendar (e.g. 2021-01-07) representation is supported for date part.
|
|
1764
|
+
* Week (e.g. 2021-W01-5 for 2021-01-07) or ordinal (e.g. 2021-007 for 2021-01-07) representations are not supported.
|
|
1765
|
+
*/
|
|
1766
|
+
class DateTimeIso8601Parse {
|
|
1767
|
+
/**
|
|
1768
|
+
* Constructor
|
|
1769
|
+
*
|
|
1770
|
+
* @constructor
|
|
1771
|
+
* @param options Options
|
|
1772
|
+
*/
|
|
1773
|
+
constructor() { }
|
|
1774
|
+
/**
|
|
1775
|
+
* Parse an ISO-8601 datetime string representation to a Date object, or throw on unparseable value.
|
|
1776
|
+
* If parser instance constructed with a timeZone option, returned Date instance will be offsetted to that time zone.
|
|
1777
|
+
*
|
|
1778
|
+
* @param value {string} Value to parse
|
|
1779
|
+
* @returns {Date} Date instance
|
|
1780
|
+
*/
|
|
1781
|
+
parse(value) {
|
|
1782
|
+
return parseDateTimeIsoString(value);
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
const iso8601Parser = new DateTimeIso8601Parse();
|
|
1787
|
+
/**
|
|
1788
|
+
* Instantiate or returns a cached value of a CLDR-based date time parser, instantiated with given a parser options.
|
|
1789
|
+
*
|
|
1790
|
+
* @param options {DateTimeIso8601ParseOptions} Parser options
|
|
1791
|
+
* @returns {DateTimeParse} Parser instance
|
|
1792
|
+
*/
|
|
1793
|
+
function getDateTimeISO8601Parser() {
|
|
1794
|
+
return iso8601Parser;
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
/**
|
|
1798
|
+
* Localizer
|
|
1799
|
+
*
|
|
1800
|
+
* @author aazizy
|
|
1801
|
+
*/
|
|
1802
|
+
function clearCache() {
|
|
1803
|
+
clearDateTimeFormatCache();
|
|
1804
|
+
clearDateTimeCLDRParserCache();
|
|
1805
|
+
clearRelativeTimeFormatCache();
|
|
1806
|
+
clearNumberFormatCache();
|
|
1807
|
+
clearNumberParserCache();
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
export { clearCache, getDateTimeCLDRParser, getDateTimeFormat, getDateTimeISO8601Parser, getNumberFormat, getNumberParser, getRelativeTimeFormat };
|