@tubular/time
Version:
Date/time, IANA timezones, leap seconds, TAI/UTC conversions, calendar with settable Julian/Gregorian switchover
1,146 lines • 56 kB
JavaScript
import { enEras, enMonths, enMonthsShort, enWeekdays, enWeekdaysMin, enWeekdaysShort, getDatePart, getDateValue, parseTimeOffset, setFormatter } from './common';
import { DateTime } from './date-time';
import { abs, floor, mod } from '@tubular/math';
import { clone, convertDigitsToAscii, flatten, forEach, isArray, isEqual, isNumber, isString, last, toNumber } from '@tubular/util';
import { checkDtfOptions, getMeridiems, getMinDaysInWeek, getOrdinals, getStartOfWeek, getWeekend, hasIntlDateTime, hasPriorityMeridiems, normalizeLocale } from './locale-data';
import { Timezone } from './timezone';
var DateTimeFormat = Intl.DateTimeFormat;
const shortOpts = { Y: 'year', M: 'month', D: 'day', w: 'weekday', h: 'hour', m: 'minute', s: 'second', z: 'timeZoneName',
ds: 'dateStyle', ts: 'timeStyle', e: 'era' };
const shortOptValues = { f: 'full', m: 'medium', n: 'narrow', s: 'short', l: 'long', dd: '2-digit', d: 'numeric' };
const styleOptValues = { F: 'full', L: 'long', M: 'medium', S: 'short' };
const patternTokens = /({[A-Za-z0-9/_]+?!?}|V|v|R|r|I[FLMSx][FLMS]?|MMMM~?|MMM~?|MM~?|Mo|M~?|Qo|Q|DDDD|DDD|Do|DD~?|D~?|dddd|ddd|do|dd|d|E|e|ww|wo|w|WW|Wo|W|YYYYYY|yyyyyy|YYYY~?|yyyy|YY|yy|Y~?|y~?|N{1,5}|n|gggg|gg|GGGG|GG|A|a|HH|H|hh|h|kk|k|mm|m|ss|s|LTS|LT|LLLL|llll|LLL|lll|LL|ll|L|l|S+|ZZZ|zzz|ZZ|zz|Z|z|XT|xt|XX|xx|X|x)/g;
const cachedLocales = {};
const invalidZones = new Set();
const warnedZones = new Set();
let allNumeric;
let dateMarkCheck;
try {
// Make sure Unicode character classes work.
allNumeric = /^\p{Nd}+$/u;
allNumeric.test('7१');
dateMarkCheck = /\x80(?=[\p{L}\p{N}])/gu;
}
catch (_a) {
allNumeric = /^\d+$/;
dateMarkCheck = /\x80(?=[a-z0-9])/g;
}
export function newDateTimeFormat(locale, options) {
options = options && checkDtfOptions(options);
for (let i = 0; i < 2; ++i) {
let orig = options;
if (options.dateStyle || options.timeStyle) {
const standardOptions = resolveFormatDetails(locale, options.dateStyle, options.timeStyle);
let changes = 0;
orig = clone(options);
delete options.dateStyle;
delete options.timeStyle;
forEach(standardOptions, (key, value) => {
if (options[key] == null)
options[key] = value;
else if (options[key] !== value)
++changes;
});
forEach(orig, key => changes += +(orig[key] !== standardOptions[key] &&
['day', 'era', 'fractionalSecondDigits', 'hour', 'minute', 'month', 'second', 'timeZoneName', 'weekday', 'year'].includes(key)));
if (changes === 0 && i !== 1)
options = orig;
}
try {
return new DateTimeFormat(locale, options);
}
catch (_a) {
options = orig;
}
}
return new DateTimeFormat(locale, options);
}
function formatEscape(s) {
let result = '';
let inAlpha = false;
s.split('').forEach(c => {
if (/[~a-z[]/i.test(c)) {
if (!inAlpha) {
inAlpha = true;
result += '[';
}
}
else if (inAlpha && c.trim().length > 0 && c.charCodeAt(0) < 128) {
inAlpha = false;
result += ']';
result = result.replace(/(\s+)]$/, ']$1');
}
result += c;
});
if (inAlpha) {
result += ']';
result = result.replace(/(\s+)]$/, ']$1');
}
return result;
}
const CACHE_LIMIT = 500;
const cachedParts = new Map();
const cachedPartsStripped = new Map();
export function decomposeFormatString(format, stripDateMarks = false) {
const cache = (stripDateMarks ? cachedPartsStripped : cachedParts);
let parts = cache.get(format);
if (parts)
return parts;
else
parts = [];
let inLiteral = true;
let inBraces = false;
let literal = '';
let token = '';
for (const ch of format.split('')) {
if (/[~a-z]/i.test(ch) || (inBraces && ch === '[')) {
if (inBraces)
literal += ch;
else if (inLiteral) {
parts.push(literal);
literal = '';
token = ch;
inLiteral = false;
}
else
token += ch;
}
else if (ch === '[') {
inBraces = true;
if (!inLiteral) {
if (stripDateMarks)
token = token.replace(/~$/, '');
parts.push(token);
token = '';
inLiteral = true;
}
}
else if (inBraces && ch === ']')
inBraces = false;
else {
if (!inLiteral) {
if (stripDateMarks && token.endsWith('~')) {
token = token.slice(0, -1);
literal += ' ';
}
parts.push(token);
token = '';
inLiteral = true;
}
literal += ch;
}
}
if ((inLiteral && literal) || (!inLiteral && token))
parts.push(literal || token);
for (let i = 1; i < parts.length; i += 2)
parts[i] = parts[i].split(patternTokens);
parts.forEach((part, index) => {
var _a;
if (index % 2 === 0)
return;
if (part.length === 3 && !part[0] && !part[2])
parts[index] = part[1];
else {
parts[index - 1] += part[0];
parts[index + 1] = last(part) + ((_a = parts[index + 1]) !== null && _a !== void 0 ? _a : '');
parts[index] = part.slice(1, part.length - 1);
}
});
parts = flatten(parts);
if (cache.size >= CACHE_LIMIT)
cache.clear();
cache.set(format, parts);
return parts;
}
function parseDateTimeFormatMods(s) {
s = s.replace(/\b([-_a-z0-9]+)\b/ig, '"$1"');
try {
return JSON.parse(s);
}
catch (_a) { }
return null;
}
function isLetter(char, checkDot = false) {
// This custom test works out better than the \p{L} character class for parsing purposes here.
return (checkDot && char === '.') ||
// eslint-disable-next-line no-misleading-character-class -- Deliberately including combining diacritical marks
/^[A-Za-zÀ-ÖØ-öø-ˁˆ-ˑˠ-ˤˬˮ\u0300-\u036FΑ-ΡΣ-ϔА-ҀҊ-ԯ\u05D0-\u05E9\u0620-\u065F\u066E-\u066F\u0671-\u06D3\u06D5\u06E5-\u06E6\u06EE-\u06EF\u06FA-\u06FC\u06FF\u0904-\u0939\u0F00-\u0F14\u0F40-\u0FBC\u1000-\u103F]/.test(char);
}
function isCased(s) {
return s.toLowerCase() !== s.toUpperCase();
}
function timeMatch(dt, locale) {
const format = locale.dateTimeFormats.check;
if (!format)
return false;
const fields = format.formatToParts(dt.epochMillis);
const wt = dt.wallTime;
return wt.hrs === getDateValue(fields, 'hour') &&
wt.min === getDateValue(fields, 'minute') &&
wt.sec === getDateValue(fields, 'second');
}
export function format(dt, fmt, localeOverride) {
var _a;
if (!dt.valid)
return '##Invalid_Date##';
const currentLocale = normalizeLocale(localeOverride !== null && localeOverride !== void 0 ? localeOverride : dt.locale);
const localeNames = !hasIntlDateTime ? 'en' : currentLocale;
const locale = getLocaleInfo(localeNames);
const cjk = /^(ja|ko|zh)/.test(locale.name);
const ko = /^ko/.test(locale.name);
const dateMarks = cjk ? ko ? ['년', '월', '일'] : ['年', '月', '日'] : ['\x80', '\x80', '\x80'];
let usesDateMarks = false;
const zeroAdj = locale.zeroDigit.charCodeAt(0) - 48;
const toNum = (n, pad = 1) => {
if (n == null || (isNumber(n) && isNaN(n)))
return '?'.repeat(pad);
else
return n.toString().padStart(pad, '0').replace(/\d/g, ch => String.fromCharCode(ch.charCodeAt(0) + zeroAdj));
};
const dtfMods = [];
fmt = fmt.replace(/(\bI[FLMSx][FLMS]?)({[^}]+})?/g, (_match, $1, $2) => {
if ($2)
dtfMods.push(parseDateTimeFormatMods($2));
else
dtfMods.push(null);
return $1;
});
const parts = decomposeFormatString(fmt);
const result = [];
const wt = dt.wallTime;
const year = wt.y;
const eraYear = abs(year) + (year <= 0 ? 1 : 0);
const month = wt.m;
const quarter = floor((month + 2) / 3);
const day = wt.d;
const hour = wt.hrs;
const h = (hour === 0 ? 12 : hour <= 12 ? hour : hour - 12);
const K = (hour < 12 ? hour : hour - 12);
const k = (hour === 0 ? 24 : hour);
const min = wt.min;
const sec = wt.sec;
const dayOfWeek = dt.getDayOfWeek();
const zoneName = dt.timezone.zoneName;
for (let i = 0; i < parts.length; i += 2) {
result.push(parts[i]);
let field = parts[i + 1];
let dateMark = 0;
if (field == null)
break;
else if (field.endsWith('~')) {
dateMark = -1;
field = field.slice(0, -1);
usesDateMarks = true;
}
if (!invalidZones.has(zoneName) &&
((/^[LlZzI]/.test(field) && locale.cachedTimezone !== zoneName) ||
(hasIntlDateTime && isEqual(locale.dateTimeFormats, {})))) {
try {
generatePredefinedFormats(locale, zoneName);
}
catch (e) {
if (/invalid time zone/i.test(e.message))
invalidZones.add(zoneName);
}
}
switch (field) {
case 'YYYYYY': // long year, always signed
case 'yyyyyy':
result.push((year < 0 ? '-' : '+') + toNum(abs(year), 6));
break;
case 'YYYY': // year, padded to at least 4 digits, signed if negative or > 9999
case 'yyyy':
case 'Y':
result.push((year < 0 ? '-' : year <= 9999 ? '' : field === 'Y' ? '+' : '') + toNum(abs(year), 4));
dateMark = dateMark && 1;
break;
case 'YY': // 2-digit year
case 'yy':
result.push(toNum(mod(abs(year), 100), 2));
break;
case 'y': // Era year, never signed, min value 1.
result.push(toNum(eraYear));
dateMark = dateMark && 1;
break;
case 'GGGG': // ISO-week year
case 'GG':
result.push((wt.yw < 0 ? '-' : year <= 9999 ? '' : field === 'GGGG' ? '+' : '') +
toNum(field.length === 2 ? abs(wt.yw) % 100 : abs(wt.yw), field.length));
break;
case 'gggg': // Locale-week year
case 'gg':
result.push((wt.ywl < 0 ? '-' : year <= 9999 ? '' : field === 'gggg' ? '+' : '') +
toNum(field.length === 2 ? abs(wt.ywl) % 100 : abs(wt.ywl), field.length));
break;
case 'Qo': // Quarter ordinal
result.push(locale.ordinals[quarter]);
break;
case 'Q': // Quarter
result.push(toNum(quarter));
break;
case 'MMMM': // Long textual month
result.push(locale.months[month - 1]);
dateMark = dateMark && 2;
break;
case 'MMM': // Short textual month
result.push(locale.monthsShort[month - 1]);
dateMark = dateMark && 2;
break;
case 'MM': // 2-digit month
result.push(toNum(month, 2));
dateMark = dateMark && 2;
break;
case 'Mo': // Month ordinal
result.push(locale.ordinals[month]);
break;
case 'M': // Numerical month
result.push(toNum(month));
dateMark = dateMark && 2;
break;
case 'WW': // ISO week number
case 'W':
result.push(toNum(wt.w, field === 'WW' ? 2 : 1));
break;
case 'ww': // Locale week number
case 'w':
result.push(toNum(wt.wl, field === 'ww' ? 2 : 1));
break;
case 'DD': // 2-digit day of month
result.push(toNum(day, 2));
dateMark = dateMark && 3;
break;
case 'Do': // Day-of-month ordinal
result.push(locale.ordinals[day]);
break;
case 'D': // Day-of-month number
result.push(toNum(day));
dateMark = dateMark && 3;
break;
case 'dddd': // Long textual day of week
result.push(locale.weekdays[dayOfWeek]);
break;
case 'ddd': // Short textual day of week
result.push(locale.weekdaysShort[dayOfWeek]);
break;
case 'dd': // Minimal textual day of week
result.push(locale.weekdaysMin[dayOfWeek]);
break;
case 'do': // Day-of-week ordinal
result.push(locale.ordinals[dayOfWeek]);
break;
case 'd': // Day-of-week number
result.push(toNum(dayOfWeek));
break;
case 'E': // Day-of-week ISO
result.push(toNum(wt.dw));
break;
case 'e': // Day-of-week locale
result.push(toNum(wt.dwl));
break;
case 'HH': // Two-digit 00-23 hour
result.push(toNum(hour, 2));
break;
case 'H': // Numeric 0-23 hour
result.push(toNum(hour));
break;
case 'hh': // Two-digit 01-12 hour
result.push(toNum(h, 2));
break;
case 'h': // Numeric 1-12 hour
result.push(toNum(h));
break;
case 'KK': // Two-digit 00-11 hour (needs AM/PM qualification)
result.push(toNum(K, 2));
break;
case 'K': // Numeric 0-11 hour (needs AM/PM qualification)
result.push(toNum(K));
break;
case 'kk': // Two-digit 01-24 hour
result.push(toNum(k, 2));
break;
case 'k': // Numeric 1-24 hour
result.push(toNum(k));
break;
case 'mm': // Two-digit minute
result.push(toNum(min, 2));
break;
case 'm': // Numeric minute
result.push(toNum(min));
break;
case 'ss': // Two-digit second
result.push(toNum(sec, 2));
break;
case 's': // Numeric second
result.push(toNum(sec));
break;
case 'A': // AM/PM indicator (may have more than just two forms)
case 'a':
{
const values = (_a = locale.meridiemAlt) !== null && _a !== void 0 ? _a : locale.meridiem;
const dayPartsForHour = values[values.length === 2 ? floor(hour / 12) : hour];
// If there is no case distinction between the first two forms, use the first form
// (the rest are there for parsing, not formatting).
if (dayPartsForHour.length === 1 ||
(!isCased(dayPartsForHour[0]) && !isCased(dayPartsForHour[0])))
result.push(dayPartsForHour[0]);
else
result.push(dayPartsForHour[field === 'A' && dayPartsForHour.length > 1 ? 1 : 0]);
}
break;
case 'XX': // Epoch 1970-01-01 00:00 seconds
result.push(dt.epochSeconds.toString());
break;
case 'xx': // Epoch 1970-01-01 00:00 milliseconds
result.push(dt.epochMillis.toString());
break;
case 'XT': // Epoch 1970-01-01 00:00 TAI seconds
result.push(dt.taiSeconds.toString());
break;
case 'xt': // Epoch 1970-01-01 00:00 TAI milliseconds
result.push(dt.taiSeconds.toString());
break;
case 'X': // Epoch 1970-01-01 00:00 UTC seconds
result.push(dt.utcSeconds.toString());
break;
case 'x': // Epoch 1970-01-01 00:00 UTC milliseconds
result.push(dt.utcMillis.toString());
break;
case 'LLLL': // Various Moment.js-style shorthand date/time formats
case 'llll':
case 'LLL':
case 'lll':
case 'LTS':
case 'LT':
case 'LL':
case 'll':
case 'L':
case 'l':
{
const localeFormat = locale.dateTimeFormats[field];
if (localeFormat == null)
result.push(`[${field}?]`);
else if (isString(localeFormat))
result.push(format(dt, localeFormat, localeOverride));
else
result.push(localeFormat.format(dt.epochMillis));
}
break;
case 'ZZZ': // As IANA zone name, if possible
if (zoneName !== 'OS') {
result.push(zoneName);
break;
}
else if (hasIntlDateTime) {
result.push(DateTimeFormat().resolvedOptions().timeZone);
break;
}
// eslint-disable-next-line no-fallthrough
case 'zzz': // As long zone name (e.g. "Pacific Daylight Time"), if possible
if (zoneName === 'TAI') {
result.push('Temps Atomique International');
break;
}
else if (hasIntlDateTime && locale.dateTimeFormats.Z instanceof DateTimeFormat) {
result.push(getDatePart(locale.dateTimeFormats.Z, dt.epochMillis, 'timeZoneName'));
break;
}
// eslint-disable-next-line no-fallthrough
case 'zz': // As zone acronym (e.g. EST, PDT, AEST), if possible
case 'z':
if (zoneName !== 'TAI' && hasIntlDateTime && locale.dateTimeFormats.z instanceof DateTimeFormat) {
result.push(getDatePart(locale.dateTimeFormats.z, dt.epochMillis, 'timeZoneName'));
break;
}
else if (invalidZones.has(zoneName)) {
result.push(dt.timezone.getDisplayName(dt.epochMillis));
break;
}
else if (zoneName !== 'OS') {
result.push(zoneName);
break;
}
else
field = 'Z';
// eslint-disable-next-line no-fallthrough
case 'ZZ': // Zone as UTC offset
case 'Z':
if (zoneName === 'TAI')
result.push(Timezone.formatUtcOffset(dt.wallTime.deltaTai, field === 'ZZ'));
else
result.push(dt.timezone.getFormattedOffset(dt.epochMillis, field === 'ZZ'));
break;
case 'V':
case 'v':
result.push(Timezone.getDstSymbol(wt.dstOffset) + (wt.dstOffset === 0 && field === 'V' ? ' ' : ''));
break;
case 'R':
case 'r':
result.push(wt.occurrence === 2 ? '\u2082' : field === 'R' ? ' ' : ''); // Subscript 2
break;
case 'n':
if (year < 1)
result.push(locale.eras[0]);
else if (result.length > 0 && last(result).endsWith(' '))
result[result.length - 1] = last(result).trimEnd();
break;
default:
if (field.startsWith('N'))
result.push(locale.eras[(year < 1 ? 0 : 1) + (field.length === 4 ? 2 : 0)]);
else if (field.startsWith('I')) {
if (hasIntlDateTime) {
const formatKey = field + (dtfMods ? JSON.stringify(dtfMods) : '');
let intlFormat = locale.dateTimeFormats[formatKey];
if (!intlFormat) {
const options = {};
const dtfMod = dtfMods.splice(0, 1)[0];
if (dtfMod)
Object.assign(options, dtfMod);
options.calendar = 'gregory';
const zone = convertDigitsToAscii(zoneName);
let $;
if (zone === 'TAI')
options.timeZone = 'UTC';
else if (($ = /^(?:GMT|UTC?)([-+])(\d\d(?::?\d\d))/.exec(zone))) {
options.timeZone = 'Etc/GMT' + ($[1] === '-' ? '+' : '-') + $[2].replace(/^0+(?=\d)|:|00$/g, '');
if (!Timezone.has(options.timeZone))
delete options.timeZone;
}
else if (zone !== 'OS')
options.timeZone = (zone === 'UT' ? 'UTC' : zone);
if (field.charAt(1) !== 'x')
options.dateStyle = styleOptValues[field.charAt(1)];
if (field.length > 2)
options.timeStyle = styleOptValues[field.charAt(2)];
try {
locale.dateTimeFormats[formatKey] = intlFormat = newDateTimeFormat(localeNames, options);
}
catch (_b) {
if (!warnedZones.has(options.timeZone)) {
console.warn('Timezone "%s" not recognized', options.timeZone);
warnedZones.add(options.timeZone);
}
delete options.timeZone;
locale.dateTimeFormats[formatKey] = intlFormat = newDateTimeFormat(localeNames, options);
}
}
if (timeMatch(dt, locale))
result.push(intlFormat.format(dt.epochMillis));
else {
// Favor @tubular/time timezone offsets over those derived from Intl.
let intlFormatAlt = locale.dateTimeFormats['_' + field];
if (!intlFormatAlt)
intlFormatAlt = locale.dateTimeFormats['_' + field] = analyzeFormat(currentLocale, intlFormat);
result.push(format(dt, intlFormatAlt, localeOverride));
}
}
else {
let intlFormat = '';
switch (field.charAt(1)) {
case 'F':
intlFormat = 'dddd, MMMM D, YYYY';
break;
case 'L':
intlFormat = 'MMMM D, YYYY';
break;
case 'M':
intlFormat = 'MMM D, YYYY';
break;
case 'S':
intlFormat = 'M/D/YY';
break;
}
if (intlFormat && /..[FLMS]/.test(field))
intlFormat += ', ';
switch (field.charAt(2)) {
case 'F':
case 'L':
intlFormat += 'h:mm:ss A zz';
break;
case 'M':
intlFormat += 'h:mm:ss A';
break;
case 'S':
intlFormat += 'h:mm A';
break;
}
result.push(format(dt, intlFormat));
}
}
else if (field.startsWith('S'))
result.push(toNum(wt.millis.toString().padStart(3, '0').substr(0, field.length), field.length));
else
result.push('??');
}
if (dateMark)
result.push(dateMarks[dateMark - 1] + (ko ? '\x80' : ''));
}
let formatted = result.join('');
if (usesDateMarks) {
if (cjk)
dateMarks.forEach(mark => formatted = formatted.replace(new RegExp(mark.repeat(2)), mark));
if (ko || !cjk)
formatted = formatted.replace(dateMarkCheck, ' ').replace(/\x80/g, '');
}
return formatted;
}
setFormatter(format);
function quickFormat(localeNames, timezone, opts) {
const options = { calendar: 'gregory' };
let $;
localeNames = normalizeLocale(localeNames);
if (timezone === 'DATELESS' || timezone === 'ZONELESS' || timezone === 'TAI')
options.timeZone = 'UTC';
else if (($ = /^(?:GMT|UTC?)([-+])(\d\d(?::?\d\d))/.exec(timezone))) {
options.timeZone = 'Etc/GMT' + ($[1] === '-' ? '+' : '-') + $[2].replace(/^0+(?=\d)|:|00$/g, '');
if (!Timezone.has(options.timeZone))
delete options.timeZone;
}
else if (timezone !== 'OS')
options.timeZone = (timezone === 'UT' ? 'UTC' : timezone);
Object.keys(opts).forEach(key => {
var _a, _b;
const value = (_a = shortOptValues[opts[key]]) !== null && _a !== void 0 ? _a : opts[key];
key = (_b = shortOpts[key]) !== null && _b !== void 0 ? _b : key;
options[key] = value;
});
try {
return newDateTimeFormat(localeNames, options);
}
catch (e) {
if (/invalid time zone/i.test(e.message)) {
const aliases = Timezone.getAliasesForZone(options.timeZone);
aliases.forEach(zone => {
try {
options.timeZone = zone;
return newDateTimeFormat(localeNames, options);
}
catch (_a) { }
});
}
throw e;
}
}
// Find the shortest case-insensitive version of each string in the array that doesn't match
// the starting characters of any other item in the array.
function shortenItems(items) {
items = items.map(item => item.toLowerCase().replace(/\u0307/g, ''));
for (let i = 0; i < items.length; ++i) {
for (let j = 1; j < items[i].length; ++j) {
const item = items[i].substr(0, j);
let matched = false;
for (let k = 0; k < items.length && !matched; ++k)
matched = (k !== i && items[k].startsWith(item));
if (!matched) {
items[i] = item;
break;
}
}
}
return items;
}
function getLocaleInfo(localeNames) {
var _a;
const joinedNames = isArray(localeNames) ? localeNames.join(',') : localeNames;
const locale = (_a = cachedLocales[joinedNames]) !== null && _a !== void 0 ? _a : {};
if (locale && Object.keys(locale).length > 0)
return locale;
const fmt = (opts) => quickFormat(localeNames, 'UTC', opts);
locale.name = isArray(localeNames) ? localeNames.join(',') : localeNames;
if (hasIntlDateTime) {
locale.months = [];
locale.monthsShort = [];
const narrow = [];
let format;
const fullTimeFormat = new DateTimeFormat(normalizeLocale(locale.name), { timeStyle: 'full', timeZone: 'UTC' });
for (let month = 1; month <= 12; ++month) {
const date = Date.UTC(2021, month - 1, 1);
let longMonth;
format = fmt({ ds: 'l' });
longMonth = getDatePart(format, date, 'month');
if (allNumeric.test(longMonth)) {
const altForm = fmt({ M: 'l' }).format(date);
if (!allNumeric.test(altForm))
longMonth = altForm;
}
locale.months.push(longMonth);
format = fmt({ ds: 'm' });
locale.monthsShort.push(getDatePart(format, date, 'month'));
format = fmt({ M: 'n' });
narrow.push(getDatePart(format, date, 'month'));
}
if (isEqual(locale.months, locale.monthsShort) && new Set(narrow).size === 12 && narrow.find(m => !/^\d+$/.test(m)))
locale.monthsShort = narrow;
locale.monthsMin = shortenItems(locale.months);
locale.monthsShortMin = shortenItems(locale.monthsShort);
locale.weekdays = [];
locale.weekdaysShort = [];
locale.weekdaysMin = [];
locale.meridiemAlt = [];
for (let day = 3; day <= 9; ++day) {
const date = Date.UTC(2021, 0, day);
format = fmt({ ds: 'f' });
locale.weekdays.push(getDatePart(format, date, 'weekday'));
format = fmt({ w: 's' });
locale.weekdaysShort.push(getDatePart(format, date, 'weekday'));
format = fmt({ w: 'n' });
locale.weekdaysMin.push(getDatePart(format, date, 'weekday'));
}
// If weekdaysMin are so narrow that there are non-unique names, try either 2 or 3 characters from weekdaysShort.
for (let len = 2; len < 4 && new Set(locale.weekdaysMin).size < 7; ++len)
locale.weekdaysMin = locale.weekdaysShort.map(name => name.substr(0, len));
const hourForms = new Set();
format = fmt({ h: 'd', hourCycle: 'h12' });
for (let hour = 0; hour < 24; ++hour) {
const date = Date.UTC(2021, 0, 1, hour, 0, 0);
const value = getDatePart(format, date, 'dayPeriod');
const lcValue = value.toLowerCase();
let newHourForm = value;
const newMeridiems = [];
if (value === lcValue)
newMeridiems.push(value);
else
newMeridiems.push(lcValue, value);
const fullValue = getDatePart(fullTimeFormat, date, 'dayPeriod');
const lcFullValue = fullValue === null || fullValue === void 0 ? void 0 : fullValue.toLowerCase();
if (fullValue && fullValue !== value) {
newHourForm += ',' + fullValue;
if (fullValue === lcFullValue)
newMeridiems.push(fullValue);
else
newMeridiems.push(lcFullValue, fullValue);
}
hourForms.add(newHourForm);
locale.meridiemAlt.push(newMeridiems);
}
if (hourForms.size < 3) {
locale.meridiemAlt.splice(13, 11);
locale.meridiemAlt.splice(1, 11);
}
locale.eras = [getDatePart(fmt({ y: 'n', e: 's' }), Date.UTC(-1, 0, 1), 'era')];
locale.eras.push(getDatePart(fmt({ y: 'n', e: 's' }), Date.UTC(1, 0, 1), 'era'));
locale.eras.push(getDatePart(fmt({ y: 'n', e: 'l' }), Date.UTC(-1, 0, 1), 'era'));
locale.eras.push(getDatePart(fmt({ y: 'n', e: 'l' }), Date.UTC(1, 0, 1), 'era'));
locale.zeroDigit = fmt({ m: 'd' }).format(0);
}
else {
locale.eras = enEras;
locale.months = enMonths;
locale.monthsMin = shortenItems(locale.months);
locale.monthsShort = enMonthsShort;
locale.monthsShortMin = shortenItems(locale.monthsShort);
locale.weekdays = enWeekdays;
locale.weekdaysShort = enWeekdaysShort;
locale.weekdaysMin = enWeekdaysMin;
locale.zeroDigit = '0';
}
locale.dateTimeFormats = {};
locale.meridiem = getMeridiems(localeNames);
locale.startOfWeek = getStartOfWeek(localeNames);
locale.minDaysInWeek = getMinDaysInWeek(localeNames);
locale.weekend = getWeekend(localeNames);
locale.ordinals = getOrdinals(localeNames);
locale.parsePatterns = {};
if (hasPriorityMeridiems(localeNames)) {
const temp = locale.meridiem;
locale.meridiem = locale.meridiemAlt;
locale.meridiemAlt = temp;
}
cachedLocales[joinedNames] = locale;
return locale;
}
function generatePredefinedFormats(locale, timezone) {
const fmt = (opts) => quickFormat(locale.name, timezone, opts);
locale.cachedTimezone = timezone;
locale.dateTimeFormats = {};
if (hasIntlDateTime) {
locale.dateTimeFormats.LLLL = fmt({ Y: 'd', M: 'l', D: 'd', w: 'l', h: 'd', m: 'dd' }); // Thursday, September 4, 1986 8:30 PM
locale.dateTimeFormats.llll = fmt({ Y: 'd', M: 's', D: 'd', w: 's', h: 'd', m: 'dd' }); // Thu, Sep 4, 1986 8:30 PM
locale.dateTimeFormats.LLL = fmt({ Y: 'd', M: 'l', D: 'd', h: 'd', m: 'dd' }); // September 4, 1986 8:30 PM
locale.dateTimeFormats.lll = fmt({ Y: 'd', M: 's', D: 'd', h: 'd', m: 'dd' }); // Sep 4, 1986 8:30 PM
locale.dateTimeFormats.LTS = fmt({ h: 'd', m: 'dd', s: 'dd' }); // 8:30:25 PM
locale.dateTimeFormats.LT = fmt({ h: 'd', m: 'dd' }); // 8:30 PM
locale.dateTimeFormats.LL = fmt({ Y: 'd', M: 'l', D: 'd' }); // September 4, 1986
locale.dateTimeFormats.ll = fmt({ Y: 'd', M: 's', D: 'd' }); // Sep 4, 1986
locale.dateTimeFormats.L = fmt({ Y: 'd', M: 'dd', D: 'dd' }); // 09/04/1986
locale.dateTimeFormats.l = fmt({ Y: 'd', M: 'd', D: 'd' }); // 9/4/1986
locale.dateTimeFormats.Z = fmt({ z: 'l', Y: 'd' }); // Don't really want the year, but without *something* else
locale.dateTimeFormats.z = fmt({ z: 's', Y: 'd' }); // a whole date appears, and just a year is easier to remove.
locale.dateTimeFormats.check = fmt({ h: 'd', m: 'd', s: 'd', hourCycle: 'h23' });
Object.keys(locale.dateTimeFormats).forEach(key => {
if (/^L/i.test(key))
locale.dateTimeFormats['_' + key] = analyzeFormat(locale.name.split(','), locale.dateTimeFormats[key]);
});
}
else {
locale.dateTimeFormats.LLLL = 'dddd, MMMM D, YYYY at h:mm A'; // Thursday, September 4, 1986 8:30 PM
locale.dateTimeFormats.llll = 'ddd, MMM D, YYYY h:mm A'; // Thu, Sep 4, 1986 8:30 PM
locale.dateTimeFormats.LLL = 'MMMM D, YYYY at h:mm A'; // September 4, 1986 8:30 PM
locale.dateTimeFormats.lll = 'MMM D, YYYY h:mm A'; // Sep 4, 1986 8:30 PM
locale.dateTimeFormats.LTS = 'h:mm:ss A'; // 8:30:25 PM
locale.dateTimeFormats.LT = 'h:mm A'; // 8:30 PM
locale.dateTimeFormats.LL = 'MMMM D, YYYY'; // September 4, 1986
locale.dateTimeFormats.ll = 'MMM D, YYYY'; // Sep 4, 1986
locale.dateTimeFormats.L = 'MM/DD/YYYY'; // 09/04/1986
locale.dateTimeFormats.l = 'M/D/YYYY'; // 9/4/1986
}
}
function isLocale(locale, matcher) {
if (isString(locale))
return locale.startsWith(matcher);
else if (locale.length > 0)
return locale[0].startsWith(matcher);
else
return false;
}
export function analyzeFormat(locale, dateStyleOrFormatter, timeStyle) {
var _a;
const options = { timeZone: 'UTC', calendar: 'gregory' };
let dateStyle;
if (dateStyleOrFormatter == null || isString(dateStyleOrFormatter)) {
if (dateStyleOrFormatter)
options.dateStyle = dateStyle = dateStyleOrFormatter;
if (timeStyle)
options.timeStyle = timeStyle;
}
else {
const formatOptions = dateStyleOrFormatter.resolvedOptions();
Object.assign(options, formatOptions);
options.timeZone = 'UTC';
dateStyle = (_a = formatOptions.dateStyle) !== null && _a !== void 0 ? _a : (options.month === 'long' ? 'long' : options.month === 'short' ? 'short' : null);
timeStyle = formatOptions.timeStyle;
}
const sampleDate = Date.UTC(2233, 3 /* 4 */, 5, 6, 7, 8);
const format = newDateTimeFormat(locale, options);
const parts = format.formatToParts(sampleDate);
const dateLong = (dateStyle === 'full' || dateStyle === 'long');
const monthLong = (dateLong || (dateStyle === 'medium' && isLocale(locale, 'ne')));
const timeFull = (timeStyle === 'full');
let formatString = '';
parts.forEach(part => {
var _a, _b;
const value = part.value = convertDigitsToAscii(part.value);
const len = value.length;
switch (part.type) {
case 'day':
formatString += 'DD'.substring(0, len);
break;
case 'dayPeriod':
formatString += 'A';
break;
case 'hour':
formatString += ((_b = { h11: 'KK', h12: 'hh', h23: 'HH', h24: 'kk' }[(_a = format.resolvedOptions().hourCycle) !== null && _a !== void 0 ? _a : 'h23']) !== null && _b !== void 0 ? _b : 'HH').substr(0, len);
break;
case 'literal':
formatString += formatEscape(value);
break;
case 'minute':
formatString += 'mm'.substring(0, len);
break;
case 'month':
if (/^\d+$/.test(value))
formatString += 'MM'.substring(0, len);
else
formatString += (monthLong ? 'MMMM' : 'MMM');
break;
case 'second':
formatString += 'ss'.substring(0, len);
break;
case 'timeZoneName':
formatString += (timeFull ? 'zzz' : 'z');
break;
case 'weekday':
formatString += (dateLong ? 'dddd' : dateStyle === 'medium' ? 'ddd' : 'dd');
break;
case 'year':
formatString += (len < 3 ? 'YY' : 'YYYY');
break;
case 'era':
formatString += 'N';
break;
}
});
return formatString;
}
const formatCache = {};
export function resolveFormatDetails(locale, dateStyle, timeStyle) {
const key = JSON.stringify(locale !== null && locale !== void 0 ? locale : null) + ';' + (dateStyle || '') + ';' + (timeStyle || '');
let result = formatCache[key];
if (result)
return result;
result = {};
const options = { timeZone: 'UTC', calendar: 'gregory',
dateStyle: dateStyle, timeStyle: timeStyle };
const sampleDate = Date.UTC(2233, 3 /* 4 */, 5, 6, 7, 8);
const format = new DateTimeFormat(locale, options);
const parts = format.formatToParts(sampleDate);
const dateLong = (dateStyle === 'full' || dateStyle === 'long');
const monthLong = (dateLong || (dateStyle === 'medium' && isLocale(locale, 'ne')));
parts.forEach(part => {
const value = part.value = convertDigitsToAscii(part.value);
const len = value.length;
const asNumber = (len === 2 ? '2-digit' : 'numeric');
switch (part.type) {
case 'day':
result.day = asNumber;
break;
case 'dayPeriod':
result.hour12 = true;
break;
case 'hour':
result.hour = asNumber;
if (format.resolvedOptions().hourCycle)
result.hourCycle = format.resolvedOptions().hourCycle;
else
result.hourCycle = format.formatToParts(Date.UTC(2233, 3 /* 4 */, 5, 13, 7, 8))['hour'] === '13' ? 'h23' : 'h12';
break;
case 'minute':
result.minute = asNumber;
break;
case 'month':
if (/^\d+$/.test(value))
result.month = asNumber;
else
result.month = (monthLong ? 'long' : 'short');
break;
case 'second':
result.second = asNumber;
break;
case 'weekday':
result.weekday = (dateLong ? 'long' : 'short');
break;
case 'year':
result.year = (len < 3 ? '2-digit' : 'numeric');
break;
case 'era':
result.era = dateStyle === 'full' ? 'short' : 'long';
break;
}
});
if (result.hourCycle)
delete result.hour12;
formatCache[key] = result;
return result;
}
function validateField(name, value, min, max) {
if (value < min || value > max)
throw new Error(`${name} value (${value}) out of range [${min}, ${max}]`);
}
function matchAmPm(locale, input) {
input = input.toLowerCase().replace(/\xA0/g, ' ');
for (const meridiem of [locale.meridiemAlt, locale.meridiem, [['am', 'a.m.', 'a. m.'], ['pm', 'p.m.', 'p. m.']]]) {
if (meridiem == null)
continue;
for (let i = 0; i < meridiem.length; ++i) {
const forms = meridiem[i];
const isPM = (i > 11 || (meridiem.length === 2 && i > 0));
for (const form of forms) {
if (input.startsWith(form.toLowerCase()))
return [isPM, form.length];
}
}
}
return [false, 0];
}
function matchEra(locale, input) {
input = input.toLowerCase().replace(/\xA0/g, ' ');
for (const eras of [locale.eras, ['BC', 'AD', 'BCE', 'CE', 'Before Christ', 'Anno Domini', 'Before Common Era', 'Common Era']]) {
if (eras == null)
continue;
for (let i = eras.length - 1; i >= 0; --i) {
const form = eras[i];
if (input.startsWith(form.toLowerCase()))
return [i % 2 === 0, form.length];
}
}
return [false, 0];
}
function matchMonth(locale, input) {
if (!locale.monthsMin || !locale.monthsShortMin)
return [0, 0];
input = input.toLowerCase().replace(/\u0307/g, '');
for (const months of [locale.monthsMin, locale.monthsShortMin]) {
let maxLen = 0;
let month = 0;
for (let i = 0; i < 12; ++i) {
const MMM = convertDigitsToAscii(months[i]);
if (MMM.length > maxLen && input.startsWith(MMM)) {
maxLen = MMM.length;
month = i + 1;
}
}
if (maxLen > 0) {
// eslint-disable-next-line no-unmodified-loop-condition
while (isLetter(input.charAt(maxLen), true))
++maxLen;
return [month, maxLen];
}
}
return [0, 0];
}
function skipDayOfWeek(locale, input) {
if (!locale.weekdays || !locale.weekdaysShort || !locale.weekdaysMin)
return 0;
input = input.toLowerCase();
for (const days of [locale.weekdays, locale.weekdaysShort, locale.weekdaysMin]) {
let maxLen = 0;
for (let i = 0; i < 7; ++i) {
const dd = days[i].toLowerCase();
if (dd.length > maxLen && input.startsWith(dd))
maxLen = dd.length;
}
if (maxLen > 0) {
// eslint-disable-next-line no-unmodified-loop-condition
while (isLetter(input.charAt(maxLen), true))
++maxLen;
return maxLen;
}
}
return 0;
}
function isNumericPart(part) {
return /^[gy]/i.test(part) || (part.length < 3 && /^[WwMDEeHhKkmsS]/.test(part));
}
export function parse(input, format, zone, locales, allowLeapSecond = false) {
var _a, _b, _c, _d, _e, _f;
let origZone = zone;
let restoreZone = false;
let occurrence = 0;
if (input.includes('₂'))
occurrence = 2;
input = convertDigitsToAscii(input.replace(/[\u00AD\u2010-\u2014\u2212]/g, '-')
.replace(/\s+/g, ' ').trim()).replace(/[\u200F₂]/g, '');
format = format.trim().replace(/\u200F/g, '');
locales = !hasIntlDateTime ? 'en' : normalizeLocale(locales !== null && locales !== void 0 ? locales : DateTime.getDefaultLocale());
if (isString(zone))
origZone = zone = Timezone.from(zone);
const locale = getLocaleInfo(locales);
let $ = /^(I[FLMSx][FLMS]?)/.exec(format);
if ($ && $[1] !== 'Ix') {
const key = $[1];
const styles = { F: 'full', L: 'long', M: 'medium', S: 'short' };
format = locale.parsePatterns[key];
if (!format) {
format = analyzeFormat(locales, styles[key.charAt(1)], styles[key.charAt(2)]);
if (!format)
return DateTime.INVALID_DATE;
format = format.replace(/\u200F/g, '');
locale.parsePatterns[key] = format;
}
}
else if (/^L(L{1,3}|TS?)$/i.test(format))
format = (_b = ((_a = locale.dateTimeFormats['_' + format]) !== null && _a !== void 0 ? _a : locale.dateTimeFormats[format])) !== null && _b !== void 0 ? _b : format;
const w = {};
const parts = decomposeFormatString(format, true);
const hasEraField = !!parts.find(part => part.toLowerCase().startsWith('n'));
const base = DateTime.getDefaultCenturyBase();
let bce = null;
let pm = null;
let pos;
let trimmed;
for (let i = 0; i < parts.length; ++i) {
let part = parts[i];
const nextPart = parts[i + 1];
if (i % 2 === 0) {
part = part.trim();
// noinspection JSNonASCIINames,NonAsciiCharacters
const altPart = { de: 'd’', 'd’': 'de' }[part];
if (input.startsWith(part))
input = input.substr(part.length).trimLeft();
else if (altPart && input.startsWith(altPart))
input = input.substr(altPart.length).trimLeft();
// Exact in-between text wasn't matched, but if the next thing coming up is a numeric field,
// just skip over the text being parsed until the next digit is found.
if (i < parts.length - 1 && isNumericPart(nextPart)) {
const $ = /^\D*(?=\d)/.exec(input);
if ($)
input = input.substr($[0].length);
else if (!/^s/i.test(nextPart))
throw new Error(`Match for "${nextPart}" field not found`);
}
continue;
}
if (part.endsWith('o'))
throw new Error('Parsing of ordinal forms is not supported');
else if (part === 'd')
throw new Error('Parsing "d" token is not supported');
let firstChar = part.substr(0, 1);
let newValueText = ((_c = /^([-+]?\d+)/.exec(input)) !== null && _c !== void 0 ? _c : [])[1];
let newValue = toNumber(newValueText);
const value2d = newValue - base % 100 + base + (newValue < base % 100 ? 100 : 0);
let handled = false;
if (newValueText != null && part.length < 3 || /[gy]/i.test(part)) {
handled = true;
switch (firstChar) {
case 'Y':
case 'y':
if (part.toLowerCase() === 'yy' && newValueText.length < 3)
w.y = value2d;
else if (bce)
w.y = 1 - newValue;
else
w.y = newValue;
if (!hasEraField && (parts[i + 2] == null || isNumericPart(parts[i + 2]))) {
firstChar = 'n';
handled = false;
input = input.substr((_d = newValueText === null || newValueText === void 0 ? void 0 : newValueText.length) !== null && _d !== void 0 ? _d : 0).trimLeft();
}
break;
case 'G':
if (part.length === 2 && newValueText.length < 3)
w.yw = value2d;
else
w.yw = newValue;
break;
case 'g':
if (part.length === 2 && newValueText.length < 3)
w.ywl = value2d;
else
w.ywl = newValue;
break;
case 'M':
validateField('month', newValue, 1, 12);
w.m = newValue;
break;
case 'W':
validateField('week-iso', newValue, 1, 53);
w.w = newValue;
break;
case 'w':
validateField('week-locale', newValue, 1, 53);
w.wl = newValue;
break;
case 'D':
validateField('date', newValue, 1, 31);
w.d = newValue;
break;
case 'E':
validateField('day-of-week-iso', newValue, 1, 7);
w.dw = newValue;
break;
case 'e':
validateField('day-of-week-locale', newValue, 1, 7);
w.dwl = newValue;
break;
case 'H':
validateField('hour-24', newValue, 0, 23);
w.hrs = newValue;
break;
case 'h':
validateField('hour-12', newValue, 1, 12);
if (pm == null)
w.hrs = newValue;
else if (pm)
w.hrs = newValue === 12 ? 12 : newValue + 12;
else
w.hrs = newValue === 12 ? 0 : newValue;
break;
case 'm':
validateField('minute', newValue, 0, 59);
w.min = newValue;
break;
case 's':
validateField('second', newValue, 0, allowLeapSecond ? 60 : 59);
w.sec = newValue;
break;
case 'S':
newValueText = newValueText.padEnd(