UNPKG

concurrently

Version:
319 lines (318 loc) 11.4 kB
"use strict"; 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'); }