concurrently
Version:
Run commands concurrently
319 lines (318 loc) • 11.4 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.DateFormatter = void 0;
/**
* Unicode-compliant date/time formatter.
*
* @see https://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns
*/
class DateFormatter {
options;
static tokenRegex = /[A-Z]/i;
parts = [];
constructor(pattern, options = {}) {
this.options = options;
let i = 0;
while (i < pattern.length) {
const char = pattern[i];
const { fn, length } = char === "'"
? this.compileLiteral(pattern, i)
: DateFormatter.tokenRegex.test(char)
? this.compileToken(pattern, i)
: this.compileOther(pattern, i);
this.parts.push(fn);
i += length;
}
}
compileLiteral(pattern, offset) {
let length = 1;
let value = '';
for (; length < pattern.length; length++) {
const i = offset + length;
const char = pattern[i];
if (char === "'") {
const nextChar = pattern[i + 1];
length++;
// if the next character is another single quote, it's been escaped.
// if not, then the literal has been closed
if (nextChar !== "'") {
break;
}
}
value += char;
}
return { fn: () => value || "'", length };
}
compileOther(pattern, offset) {
let value = '';
while (!DateFormatter.tokenRegex.test(pattern[offset]) && pattern[offset] !== "'") {
value += pattern[offset++];
}
return { fn: () => value, length: value.length };
}
compileToken(pattern, offset) {
const type = pattern[offset];
const token = tokens.get(type);
if (!token) {
throw new SyntaxError(`Formatting token "${type}" is invalid`);
}
let length = 0;
while (pattern[offset + length] === type) {
length++;
}
const tokenFn = token[length - 1];
if (!tokenFn) {
throw new RangeError(`Formatting token "${type.repeat(length)}" is unsupported`);
}
return { fn: tokenFn, length };
}
format(date) {
return this.parts.reduce((output, part) => output + String(part(date, this.options)), '');
}
}
exports.DateFormatter = DateFormatter;
/**
* A map of token to its implementations by length.
* If an index is undefined, then that token length is unsupported.
*/
const tokens = new Map()
// era
.set('G', [
makeTokenFn({ era: 'short' }, 'era'),
makeTokenFn({ era: 'short' }, 'era'),
makeTokenFn({ era: 'short' }, 'era'),
makeTokenFn({ era: 'long' }, 'era'),
makeTokenFn({ era: 'narrow' }, 'era'),
])
// year
.set('y', [
// TODO: does not support BC years.
// https://stackoverflow.com/a/41345095/2083599
(date) => date.getFullYear(),
(date) => pad(2, date.getFullYear()).slice(-2),
(date) => pad(3, date.getFullYear()),
(date) => pad(4, date.getFullYear()),
(date) => pad(5, date.getFullYear()),
])
.set('Y', [
getWeekYear,
(date, options) => pad(2, getWeekYear(date, options)).slice(-2),
(date, options) => pad(3, getWeekYear(date, options)),
(date, options) => pad(4, getWeekYear(date, options)),
(date, options) => pad(5, getWeekYear(date, options)),
])
.set('u', [])
.set('U', [
// Fallback implemented as yearName is not available in gregorian calendars, for instance.
makeTokenFn({ dateStyle: 'full' }, 'yearName', (date) => String(date.getFullYear())),
])
.set('r', [
// Fallback implemented as relatedYear is not available in gregorian calendars, for instance.
makeTokenFn({ dateStyle: 'full' }, 'relatedYear', (date) => String(date.getFullYear())),
])
// quarter
.set('Q', [
(date) => Math.floor(date.getMonth() / 3) + 1,
(date) => pad(2, Math.floor(date.getMonth() / 3) + 1),
// these aren't localized in Intl.DateTimeFormat.
undefined,
undefined,
(date) => Math.floor(date.getMonth() / 3) + 1,
])
.set('q', [
(date) => Math.floor(date.getMonth() / 3) + 1,
(date) => pad(2, Math.floor(date.getMonth() / 3) + 1),
// these aren't localized in Intl.DateTimeFormat.
undefined,
undefined,
(date) => Math.floor(date.getMonth() / 3) + 1,
])
// month
.set('M', [
(date) => date.getMonth() + 1,
(date) => pad(2, date.getMonth() + 1),
// these include the day so that it forces non-stand-alone month part
makeTokenFn({ day: 'numeric', month: 'short' }, 'month'),
makeTokenFn({ day: 'numeric', month: 'long' }, 'month'),
makeTokenFn({ day: 'numeric', month: 'narrow' }, 'month'),
])
.set('L', [
(date) => date.getMonth() + 1,
(date) => pad(2, date.getMonth() + 1),
makeTokenFn({ month: 'short' }, 'month'),
makeTokenFn({ month: 'long' }, 'month'),
makeTokenFn({ month: 'narrow' }, 'month'),
])
.set('l', [() => ''])
// week
.set('w', [getWeek, (date, options) => pad(2, getWeek(date, options))])
.set('W', [getWeekOfMonth])
// day
.set('d', [(date) => date.getDate(), (date) => pad(2, date.getDate())])
.set('D', [
getDayOfYear,
(date) => pad(2, getDayOfYear(date)),
(date) => pad(3, getDayOfYear(date)),
])
.set('F', [(date) => Math.ceil(date.getDate() / 7)])
.set('g', [])
// week day
.set('E', [
makeTokenFn({ weekday: 'short' }, 'weekday'),
makeTokenFn({ weekday: 'short' }, 'weekday'),
makeTokenFn({ weekday: 'short' }, 'weekday'),
makeTokenFn({ weekday: 'long' }, 'weekday'),
])
.set('e', [
undefined,
undefined,
makeTokenFn({ weekday: 'short' }, 'weekday'),
makeTokenFn({ weekday: 'long' }, 'weekday'),
])
.set('c', [])
// period
.set('a', [
makeTokenFn({ hour12: true, timeStyle: 'full' }, 'dayPeriod'),
makeTokenFn({ hour12: true, timeStyle: 'full' }, 'dayPeriod'),
makeTokenFn({ hour12: true, timeStyle: 'full' }, 'dayPeriod'),
])
.set('b', [])
.set('B', [
makeTokenFn({ dayPeriod: 'short' }, 'dayPeriod'),
makeTokenFn({ dayPeriod: 'short' }, 'dayPeriod'),
makeTokenFn({ dayPeriod: 'short' }, 'dayPeriod'),
makeTokenFn({ dayPeriod: 'long' }, 'dayPeriod'),
])
// hour
.set('h', [(date) => date.getHours() % 12 || 12, (date) => pad(2, date.getHours() % 12 || 12)])
.set('H', [(date) => date.getHours(), (date) => pad(2, date.getHours())])
.set('K', [(date) => date.getHours() % 12, (date) => pad(2, date.getHours() % 12)])
.set('k', [(date) => date.getHours() % 24 || 24, (date) => pad(2, date.getHours() % 24 || 24)])
.set('j', [])
.set('J', [])
.set('C', [])
// minute
.set('m', [(date) => date.getMinutes(), (date) => pad(2, date.getMinutes())])
// second
.set('s', [(date) => date.getSeconds(), (date) => pad(2, date.getSeconds())])
.set('S', [
(date) => Math.trunc(date.getMilliseconds() / 100),
(date) => pad(2, Math.trunc(date.getMilliseconds() / 10)),
(date) => pad(3, Math.trunc(date.getMilliseconds())),
])
.set('A', [])
// zone
// none of these have tests
.set('z', [
makeTokenFn({ timeZoneName: 'short' }, 'timeZoneName'),
makeTokenFn({ timeZoneName: 'short' }, 'timeZoneName'),
makeTokenFn({ timeZoneName: 'short' }, 'timeZoneName'),
makeTokenFn({ timeZoneName: 'long' }, 'timeZoneName'),
])
.set('Z', [
undefined,
undefined,
undefined,
// equivalent to `OOOO`.
makeTokenFn({ timeZoneName: 'longOffset' }, 'timeZoneName'),
])
.set('O', [
makeTokenFn({ timeZoneName: 'shortOffset' }, 'timeZoneName'),
undefined,
undefined,
// equivalent to `ZZZZ`.
makeTokenFn({ timeZoneName: 'longOffset' }, 'timeZoneName'),
])
.set('v', [
makeTokenFn({ timeZoneName: 'shortGeneric' }, 'timeZoneName'),
undefined,
undefined,
makeTokenFn({ timeZoneName: 'longGeneric' }, 'timeZoneName'),
])
.set('V', [])
.set('X', [])
.set('x', []);
let locale;
function getLocale(options) {
if (!locale || locale.baseName !== options.locale) {
locale = new Intl.Locale(options.locale || new Intl.DateTimeFormat().resolvedOptions().locale);
}
return locale;
}
/**
* Creates a token formatting function that returns the value of the chosen part type,
* using the current locale's settings.
*
* If the date/formatter settings doesn't include the requested part type,
* the `fallback` function is invoked, if specified. If none has been specified, returns an
* empty string.
*/
function makeTokenFn(options, type, fallback) {
let formatter;
return (date, formatterOptions) => {
// Allow tests to set a different locale and have that cause the formatter to be recreated
if (!formatter ||
formatter.resolvedOptions().locale !== formatterOptions.locale ||
formatter.resolvedOptions().calendar !== formatterOptions.calendar) {
formatter = new Intl.DateTimeFormat(formatterOptions.locale, {
...options,
calendar: options.calendar ?? formatterOptions.calendar,
});
}
const parts = formatter.formatToParts(date);
const part = parts.find((p) => p.type === type);
return part?.value ?? (fallback ? fallback(date, formatterOptions) : '');
};
}
function startOfWeek(date, options) {
const locale = getLocale(options);
const firstDay = locale.weekInfo.firstDay === 7 ? 0 : locale.weekInfo.firstDay;
const day = date.getDay();
const diff = (day < firstDay ? 7 : 0) + day - firstDay;
date.setDate(date.getDate() - diff);
date.setHours(0, 0, 0, 0);
return date;
}
function getWeekYear(date, options) {
const locale = getLocale(options);
const minimalDays = locale.weekInfo.minimalDays;
const year = date.getFullYear();
const thisYear = startOfWeek(new Date(year, 0, minimalDays), options);
const nextYear = startOfWeek(new Date(year + 1, 0, minimalDays), options);
if (date.getTime() >= nextYear.getTime()) {
return year + 1;
}
else if (date.getTime() >= thisYear.getTime()) {
return year;
}
else {
return year - 1;
}
}
function getWeek(date, options) {
const locale = getLocale(options);
const weekMs = 7 * 24 * 3600 * 1000;
const temp = startOfWeek(new Date(date), options);
const thisYear = new Date(getWeekYear(date, options), 0, locale.weekInfo.minimalDays);
startOfWeek(thisYear, options);
const diff = temp.getTime() - thisYear.getTime();
return Math.round(diff / weekMs) + 1;
}
function getWeekOfMonth(date, options) {
const current = new Date(date);
current.setHours(0, 0, 0, 0);
const monthWeekStart = startOfWeek(new Date(date.getFullYear(), date.getMonth(), 1), options);
const weekMs = 7 * 24 * 3600 * 1000;
return Math.floor((date.getTime() - monthWeekStart.getTime()) / weekMs) + 1;
}
function getDayOfYear(date) {
let days = 0;
for (let i = 0; i <= date.getMonth() - 1; i++) {
const temp = new Date(date.getFullYear(), i + 1, 0, 0, 0, 0);
days += temp.getDate();
}
return days + date.getDate();
}
function pad(length, val) {
return String(val).padStart(length, '0');
}
;