UNPKG

luxon

Version:
2,212 lines (1,806 loc) 201 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); // these aren't really private, but nor are they really useful to document /** * @private */ class LuxonError extends Error {} /** * @private */ class InvalidDateTimeError extends LuxonError { constructor(reason) { super(`Invalid DateTime: ${reason.toMessage()}`); } } /** * @private */ class InvalidIntervalError extends LuxonError { constructor(reason) { super(`Invalid Interval: ${reason.toMessage()}`); } } /** * @private */ class InvalidDurationError extends LuxonError { constructor(reason) { super(`Invalid Duration: ${reason.toMessage()}`); } } /** * @private */ class ConflictingSpecificationError extends LuxonError {} /** * @private */ class InvalidUnitError extends LuxonError { constructor(unit) { super(`Invalid unit ${unit}`); } } /** * @private */ class InvalidArgumentError extends LuxonError {} /** * @private */ class ZoneIsAbstractError extends LuxonError { constructor() { super("Zone is an abstract class"); } } /* This is just a junk drawer, containing anything used across multiple classes. Because Luxon is small(ish), this should stay small and we won't worry about splitting it up into, say, parsingUtil.js and basicUtil.js and so on. But they are divided up by feature area. */ /** * @private */ // TYPES function isUndefined(o) { return typeof o === "undefined"; } function isNumber(o) { return typeof o === "number"; } function isString(o) { return typeof o === "string"; } function isDate(o) { return Object.prototype.toString.call(o) === "[object Date]"; } // CAPABILITIES function hasIntl() { return typeof Intl !== "undefined" && Intl.DateTimeFormat; } function hasFormatToParts() { return !isUndefined(Intl.DateTimeFormat.prototype.formatToParts); } function hasRelative() { return typeof Intl !== "undefined" && !!Intl.RelativeTimeFormat; } // OBJECTS AND ARRAYS function maybeArray(thing) { return Array.isArray(thing) ? thing : [thing]; } function bestBy(arr, by, compare) { if (arr.length === 0) { return undefined; } return arr.reduce((best, next) => { const pair = [by(next), next]; if (!best) { return pair; } else if (compare(best[0], pair[0]) === best[0]) { return best; } else { return pair; } }, null)[1]; } function pick(obj, keys) { return keys.reduce((a, k) => { a[k] = obj[k]; return a; }, {}); } // NUMBERS AND STRINGS function numberBetween(thing, bottom, top) { return isNumber(thing) && thing >= bottom && thing <= top; } // x % n but takes the sign of n instead of x function floorMod(x, n) { return x - n * Math.floor(x / n); } function padStart(input, n = 2) { if (input.toString().length < n) { return ("0".repeat(n) + input).slice(-n); } else { return input.toString(); } } function parseInteger(string) { if (isUndefined(string) || string === null || string === "") { return undefined; } else { return parseInt(string, 10); } } function parseMillis(fraction) { // Return undefined (instead of 0) in these cases, where fraction is not set if (isUndefined(fraction) || fraction === null || fraction === "") { return undefined; } else { const f = parseFloat("0." + fraction) * 1000; return Math.floor(f); } } function roundTo(number, digits, towardZero = false) { const factor = Math.pow(10, digits), rounder = towardZero ? Math.trunc : Math.round; return rounder(number * factor) / factor; } // DATE BASICS function isLeapYear(year) { return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); } function daysInYear(year) { return isLeapYear(year) ? 366 : 365; } function daysInMonth(year, month) { const modMonth = floorMod(month - 1, 12) + 1, modYear = year + (month - modMonth) / 12; if (modMonth === 2) { return isLeapYear(modYear) ? 29 : 28; } else { return [31, null, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][modMonth - 1]; } } // covert a calendar object to a local timestamp (epoch, but with the offset baked in) function objToLocalTS(obj) { let d = Date.UTC(obj.year, obj.month - 1, obj.day, obj.hour, obj.minute, obj.second, obj.millisecond); // for legacy reasons, years between 0 and 99 are interpreted as 19XX; revert that if (obj.year < 100 && obj.year >= 0) { d = new Date(d); d.setUTCFullYear(d.getUTCFullYear() - 1900); } return +d; } function weeksInWeekYear(weekYear) { const p1 = (weekYear + Math.floor(weekYear / 4) - Math.floor(weekYear / 100) + Math.floor(weekYear / 400)) % 7, last = weekYear - 1, p2 = (last + Math.floor(last / 4) - Math.floor(last / 100) + Math.floor(last / 400)) % 7; return p1 === 4 || p2 === 3 ? 53 : 52; } function untruncateYear(year) { if (year > 99) { return year; } else return year > 60 ? 1900 + year : 2000 + year; } // PARSING function parseZoneInfo(ts, offsetFormat, locale, timeZone = null) { const date = new Date(ts), intlOpts = { hour12: false, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" }; if (timeZone) { intlOpts.timeZone = timeZone; } const modified = Object.assign({ timeZoneName: offsetFormat }, intlOpts), intl = hasIntl(); if (intl && hasFormatToParts()) { const parsed = new Intl.DateTimeFormat(locale, modified).formatToParts(date).find(m => m.type.toLowerCase() === "timezonename"); return parsed ? parsed.value : null; } else if (intl) { // this probably doesn't work for all locales const without = new Intl.DateTimeFormat(locale, intlOpts).format(date), included = new Intl.DateTimeFormat(locale, modified).format(date), diffed = included.substring(without.length), trimmed = diffed.replace(/^[, \u200e]+/, ""); return trimmed; } else { return null; } } // signedOffset('-5', '30') -> -330 function signedOffset(offHourStr, offMinuteStr) { const offHour = parseInt(offHourStr, 10) || 0, offMin = parseInt(offMinuteStr, 10) || 0, offMinSigned = offHour < 0 ? -offMin : offMin; return offHour * 60 + offMinSigned; } // COERCION function asNumber(value) { const numericValue = Number(value); if (typeof value === "boolean" || value === "" || Number.isNaN(numericValue)) throw new InvalidArgumentError(`Invalid unit value ${value}`); return numericValue; } function normalizeObject(obj, normalizer, nonUnitKeys) { const normalized = {}; for (const u in obj) { if (obj.hasOwnProperty(u)) { if (nonUnitKeys.indexOf(u) >= 0) continue; const v = obj[u]; if (v === undefined || v === null) continue; normalized[normalizer(u)] = asNumber(v); } } return normalized; } function timeObject(obj) { return pick(obj, ["hour", "minute", "second", "millisecond"]); } const ianaRegex = /[A-Za-z_+-]{1,256}(:?\/[A-Za-z_+-]{1,256}(\/[A-Za-z_+-]{1,256})?)?/; /** * @private */ const n = "numeric", s = "short", l = "long", d2 = "2-digit"; const DATE_SHORT = { year: n, month: n, day: n }; const DATE_MED = { year: n, month: s, day: n }; const DATE_FULL = { year: n, month: l, day: n }; const DATE_HUGE = { year: n, month: l, day: n, weekday: l }; const TIME_SIMPLE = { hour: n, minute: d2 }; const TIME_WITH_SECONDS = { hour: n, minute: d2, second: d2 }; const TIME_WITH_SHORT_OFFSET = { hour: n, minute: d2, second: d2, timeZoneName: s }; const TIME_WITH_LONG_OFFSET = { hour: n, minute: d2, second: d2, timeZoneName: l }; const TIME_24_SIMPLE = { hour: n, minute: d2, hour12: false }; /** * {@link toLocaleString}; format like '09:30:23', always 24-hour. */ const TIME_24_WITH_SECONDS = { hour: n, minute: d2, second: d2, hour12: false }; /** * {@link toLocaleString}; format like '09:30:23 EDT', always 24-hour. */ const TIME_24_WITH_SHORT_OFFSET = { hour: n, minute: d2, second: d2, hour12: false, timeZoneName: s }; /** * {@link toLocaleString}; format like '09:30:23 Eastern Daylight Time', always 24-hour. */ const TIME_24_WITH_LONG_OFFSET = { hour: n, minute: d2, second: d2, hour12: false, timeZoneName: l }; /** * {@link toLocaleString}; format like '10/14/1983, 9:30 AM'. Only 12-hour if the locale is. */ const DATETIME_SHORT = { year: n, month: n, day: n, hour: n, minute: d2 }; /** * {@link toLocaleString}; format like '10/14/1983, 9:30:33 AM'. Only 12-hour if the locale is. */ const DATETIME_SHORT_WITH_SECONDS = { year: n, month: n, day: n, hour: n, minute: d2, second: d2 }; const DATETIME_MED = { year: n, month: s, day: n, hour: n, minute: d2 }; const DATETIME_MED_WITH_SECONDS = { year: n, month: s, day: n, hour: n, minute: d2, second: d2 }; const DATETIME_FULL = { year: n, month: l, day: n, hour: n, minute: d2, timeZoneName: s }; const DATETIME_FULL_WITH_SECONDS = { year: n, month: l, day: n, hour: n, minute: d2, second: d2, timeZoneName: s }; const DATETIME_HUGE = { year: n, month: l, day: n, weekday: l, hour: n, minute: d2, timeZoneName: l }; const DATETIME_HUGE_WITH_SECONDS = { year: n, month: l, day: n, weekday: l, hour: n, minute: d2, second: d2, timeZoneName: l }; function stringify(obj) { return JSON.stringify(obj, Object.keys(obj).sort()); } /** * @private */ const monthsLong = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; const monthsShort = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; const monthsNarrow = ["J", "F", "M", "A", "M", "J", "J", "A", "S", "O", "N", "D"]; function months(length) { switch (length) { case "narrow": return monthsNarrow; case "short": return monthsShort; case "long": return monthsLong; case "numeric": return ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]; case "2-digit": return ["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"]; default: return null; } } const weekdaysLong = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]; const weekdaysShort = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; const weekdaysNarrow = ["M", "T", "W", "T", "F", "S", "S"]; function weekdays(length) { switch (length) { case "narrow": return weekdaysNarrow; case "short": return weekdaysShort; case "long": return weekdaysLong; case "numeric": return ["1", "2", "3", "4", "5", "6", "7"]; default: return null; } } const meridiems = ["AM", "PM"]; const erasLong = ["Before Christ", "Anno Domini"]; const erasShort = ["BC", "AD"]; const erasNarrow = ["B", "A"]; function eras(length) { switch (length) { case "narrow": return erasNarrow; case "short": return erasShort; case "long": return erasLong; default: return null; } } function meridiemForDateTime(dt) { return meridiems[dt.hour < 12 ? 0 : 1]; } function weekdayForDateTime(dt, length) { return weekdays(length)[dt.weekday - 1]; } function monthForDateTime(dt, length) { return months(length)[dt.month - 1]; } function eraForDateTime(dt, length) { return eras(length)[dt.year < 0 ? 0 : 1]; } function formatRelativeTime(unit, count, numeric = "always", narrow = false) { const units = { years: ["year", "yr."], quarters: ["quarer", "qtr."], months: ["month", "mo."], weeks: ["week", "wk."], days: ["day", "day"], hours: ["hour", "hr."], minutes: ["minute", "min."], seconds: ["second", "sec."] }; const lastable = ["hours", "minutes", "seconds"].indexOf(unit) === -1; if (numeric === "auto" && lastable) { const isDay = unit === "days"; switch (count) { case 1: return isDay ? "tomorrow" : `next ${units[unit][0]}`; case -1: return isDay ? "yesterday" : `last ${units[unit][0]}`; case 0: return isDay ? "today" : `this ${units[unit][0]}`; default: // fall through } } const isInPast = Object.is(count, -0) || count < 0, fmtValue = Math.abs(count), fmtUnit = narrow ? units[unit][1] : fmtValue === 1 ? units[unit][0] : unit; return isInPast ? `${fmtValue} ${fmtUnit} ago` : `in ${fmtValue} ${fmtUnit}`; } function formatString(knownFormat) { // these all have the offsets removed because we don't have access to them // without all the intl stuff this is backfilling const filtered = pick(knownFormat, ["weekday", "era", "year", "month", "day", "hour", "minute", "second", "timeZoneName", "hour12"]), key = stringify(filtered), dateTimeHuge = "EEEE, LLLL d, yyyy, h:mm a"; switch (key) { case stringify(DATE_SHORT): return "M/d/yyyy"; case stringify(DATE_MED): return "LLL d, yyyy"; case stringify(DATE_FULL): return "LLLL d, yyyy"; case stringify(DATE_HUGE): return "EEEE, LLLL d, yyyy"; case stringify(TIME_SIMPLE): return "h:mm a"; case stringify(TIME_WITH_SECONDS): return "h:mm:ss a"; case stringify(TIME_WITH_SHORT_OFFSET): return "h:mm a"; case stringify(TIME_WITH_LONG_OFFSET): return "h:mm a"; case stringify(TIME_24_SIMPLE): return "HH:mm"; case stringify(TIME_24_WITH_SECONDS): return "HH:mm:ss"; case stringify(TIME_24_WITH_SHORT_OFFSET): return "HH:mm"; case stringify(TIME_24_WITH_LONG_OFFSET): return "HH:mm"; case stringify(DATETIME_SHORT): return "M/d/yyyy, h:mm a"; case stringify(DATETIME_MED): return "LLL d, yyyy, h:mm a"; case stringify(DATETIME_FULL): return "LLLL d, yyyy, h:mm a"; case stringify(DATETIME_HUGE): return dateTimeHuge; case stringify(DATETIME_SHORT_WITH_SECONDS): return "M/d/yyyy, h:mm:ss a"; case stringify(DATETIME_MED_WITH_SECONDS): return "LLL d, yyyy, h:mm:ss a"; case stringify(DATETIME_FULL_WITH_SECONDS): return "LLLL d, yyyy, h:mm:ss a"; case stringify(DATETIME_HUGE_WITH_SECONDS): return "EEEE, LLLL d, yyyy, h:mm:ss a"; default: return dateTimeHuge; } } /* eslint no-unused-vars: "off" */ /** * @interface */ class Zone { /** * The type of zone * @abstract * @type {string} */ get type() { throw new ZoneIsAbstractError(); } /** * The name of this zone. * @abstract * @type {string} */ get name() { throw new ZoneIsAbstractError(); } /** * Returns whether the offset is known to be fixed for the whole year. * @abstract * @type {boolean} */ get universal() { throw new ZoneIsAbstractError(); } /** * Returns the offset's common name (such as EST) at the specified timestamp * @abstract * @param {number} ts - Epoch milliseconds for which to get the name * @param {Object} opts - Options to affect the format * @param {string} opts.format - What style of offset to return. Accepts 'long' or 'short'. * @param {string} opts.locale - What locale to return the offset name in. * @return {string} */ offsetName(ts, opts) { throw new ZoneIsAbstractError(); } /** * Return the offset in minutes for this zone at the specified timestamp. * @abstract * @param {number} ts - Epoch milliseconds for which to compute the offset * @return {number} */ offset(ts) { throw new ZoneIsAbstractError(); } /** * Return whether this Zone is equal to another zone * @abstract * @param {Zone} otherZone - the zone to compare * @return {boolean} */ equals(otherZone) { throw new ZoneIsAbstractError(); } /** * Return whether this Zone is valid. * @abstract * @type {boolean} */ get isValid() { throw new ZoneIsAbstractError(); } } let singleton = null; /** * Represents the local zone for this Javascript environment. * @implements {Zone} */ class LocalZone extends Zone { /** * Get a singleton instance of the local zone * @return {LocalZone} */ static get instance() { if (singleton === null) { singleton = new LocalZone(); } return singleton; } /** @override **/ get type() { return "local"; } /** @override **/ get name() { if (hasIntl()) { return new Intl.DateTimeFormat().resolvedOptions().timeZone; } else return "local"; } /** @override **/ get universal() { return false; } /** @override **/ offsetName(ts, { format, locale }) { return parseZoneInfo(ts, format, locale); } /** @override **/ offset(ts) { return -new Date(ts).getTimezoneOffset(); } /** @override **/ equals(otherZone) { return otherZone.type === "local"; } /** @override **/ get isValid() { return true; } } const matchingRegex = RegExp(`^${ianaRegex.source}$`); let dtfCache = {}; function makeDTF(zone) { if (!dtfCache[zone]) { dtfCache[zone] = new Intl.DateTimeFormat("en-US", { hour12: false, timeZone: zone, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit" }); } return dtfCache[zone]; } const typeToPos = { year: 0, month: 1, day: 2, hour: 3, minute: 4, second: 5 }; function hackyOffset(dtf, date) { const formatted = dtf.format(date).replace(/\u200E/g, ""), parsed = /(\d+)\/(\d+)\/(\d+),? (\d+):(\d+):(\d+)/.exec(formatted), [, fMonth, fDay, fYear, fHour, fMinute, fSecond] = parsed; return [fYear, fMonth, fDay, fHour, fMinute, fSecond]; } function partsOffset(dtf, date) { const formatted = dtf.formatToParts(date), filled = []; for (let i = 0; i < formatted.length; i++) { const { type, value } = formatted[i], pos = typeToPos[type]; if (!isUndefined(pos)) { filled[pos] = parseInt(value, 10); } } return filled; } let ianaZoneCache = {}; /** * A zone identified by an IANA identifier, like America/New_York * @implements {Zone} */ class IANAZone extends Zone { /** * @param {string} name - Zone name * @return {IANAZone} */ static create(name) { if (!ianaZoneCache[name]) { ianaZoneCache[name] = new IANAZone(name); } return ianaZoneCache[name]; } /** * Reset local caches. Should only be necessary in testing scenarios. * @return {void} */ static resetCache() { ianaZoneCache = {}; dtfCache = {}; } /** * Returns whether the provided string is a valid specifier. This only checks the string's format, not that the specifier identifies a known zone; see isValidZone for that. * @param {string} s - The string to check validity on * @example IANAZone.isValidSpecifier("America/New_York") //=> true * @example IANAZone.isValidSpecifier("Fantasia/Castle") //=> true * @example IANAZone.isValidSpecifier("Sport~~blorp") //=> false * @return {boolean} */ static isValidSpecifier(s) { return s && s.match(matchingRegex); } /** * Returns whether the provided string identifies a real zone * @param {string} zone - The string to check * @example IANAZone.isValidZone("America/New_York") //=> true * @example IANAZone.isValidZone("Fantasia/Castle") //=> false * @example IANAZone.isValidZone("Sport~~blorp") //=> false * @return {boolean} */ static isValidZone(zone) { try { new Intl.DateTimeFormat("en-US", { timeZone: zone }).format(); return true; } catch (e) { return false; } } // Etc/GMT+8 -> -480 /** @ignore */ static parseGMTOffset(specifier) { if (specifier) { const match = specifier.match(/^Etc\/GMT([+-]\d{1,2})$/i); if (match) { return -60 * parseInt(match[1]); } } return null; } constructor(name) { super(); /** @private **/ this.zoneName = name; /** @private **/ this.valid = IANAZone.isValidZone(name); } /** @override **/ get type() { return "iana"; } /** @override **/ get name() { return this.zoneName; } /** @override **/ get universal() { return false; } /** @override **/ offsetName(ts, { format, locale }) { return parseZoneInfo(ts, format, locale, this.name); } /** @override **/ offset(ts) { const date = new Date(ts), dtf = makeDTF(this.name), [year, month, day, hour, minute, second] = dtf.formatToParts ? partsOffset(dtf, date) : hackyOffset(dtf, date); const asUTC = objToLocalTS({ year, month, day, hour, minute, second, millisecond: 0 }); let asTS = date.valueOf(); asTS -= asTS % 1000; return (asUTC - asTS) / (60 * 1000); } /** @override **/ equals(otherZone) { return otherZone.type === "iana" && otherZone.name === this.name; } /** @override **/ get isValid() { return this.valid; } } let singleton$1 = null; function hoursMinutesOffset(z) { const hours = Math.trunc(z.fixed / 60), minutes = Math.abs(z.fixed % 60), sign = hours > 0 ? "+" : "-", base = sign + Math.abs(hours); return minutes > 0 ? `${base}:${padStart(minutes, 2)}` : base; } /** * A zone with a fixed offset (i.e. no DST) * @implements {Zone} */ class FixedOffsetZone extends Zone { /** * Get a singleton instance of UTC * @return {FixedOffsetZone} */ static get utcInstance() { if (singleton$1 === null) { singleton$1 = new FixedOffsetZone(0); } return singleton$1; } /** * Get an instance with a specified offset * @param {number} offset - The offset in minutes * @return {FixedOffsetZone} */ static instance(offset) { return offset === 0 ? FixedOffsetZone.utcInstance : new FixedOffsetZone(offset); } /** * Get an instance of FixedOffsetZone from a UTC offset string, like "UTC+6" * @param {string} s - The offset string to parse * @example FixedOffsetZone.parseSpecifier("UTC+6") * @example FixedOffsetZone.parseSpecifier("UTC+06") * @example FixedOffsetZone.parseSpecifier("UTC-6:00") * @return {FixedOffsetZone} */ static parseSpecifier(s) { if (s) { const r = s.match(/^utc(?:([+-]\d{1,2})(?::(\d{2}))?)?$/i); if (r) { return new FixedOffsetZone(signedOffset(r[1], r[2])); } } return null; } constructor(offset) { super(); /** @private **/ this.fixed = offset; } /** @override **/ get type() { return "fixed"; } /** @override **/ get name() { return this.fixed === 0 ? "UTC" : `UTC${hoursMinutesOffset(this)}`; } /** @override **/ offsetName() { return this.name; } /** @override **/ get universal() { return true; } /** @override **/ offset() { return this.fixed; } /** @override **/ equals(otherZone) { return otherZone.type === "fixed" && otherZone.fixed === this.fixed; } /** @override **/ get isValid() { return true; } } /** * A zone that failed to parse. You should never need to instantiate this. * @implements {Zone} */ class InvalidZone extends Zone { constructor(zoneName) { super(); /** @private */ this.zoneName = zoneName; } /** @override **/ get type() { return "invalid"; } /** @override **/ get name() { return this.zoneName; } /** @override **/ get universal() { return false; } /** @override **/ offsetName() { return null; } /** @override **/ offset() { return NaN; } /** @override **/ equals() { return false; } /** @override **/ get isValid() { return false; } } /** * @private */ function normalizeZone(input, defaultZone) { let offset; if (isUndefined(input) || input === null) { return defaultZone; } else if (input instanceof Zone) { return input; } else if (isString(input)) { const lowered = input.toLowerCase(); if (lowered === "local") return defaultZone;else if (lowered === "utc" || lowered === "gmt") return FixedOffsetZone.utcInstance;else if ((offset = IANAZone.parseGMTOffset(input)) != null) { // handle Etc/GMT-4, which V8 chokes on return FixedOffsetZone.instance(offset); } else if (IANAZone.isValidSpecifier(lowered)) return IANAZone.create(input);else return FixedOffsetZone.parseSpecifier(lowered) || new InvalidZone(input); } else if (isNumber(input)) { return FixedOffsetZone.instance(input); } else if (typeof input === "object" && input.offset && typeof input.offset === "number") { // This is dumb, but the instanceof check above doesn't seem to really work // so we're duck checking it return input; } else { return new InvalidZone(input); } } let now = () => Date.now(), defaultZone = null, // not setting this directly to LocalZone.instance bc loading order issues defaultLocale = null, defaultNumberingSystem = null, defaultOutputCalendar = null, throwOnInvalid = false; /** * Settings contains static getters and setters that control Luxon's overall behavior. Luxon is a simple library with few options, but the ones it does have live here. */ class Settings { /** * Get the callback for returning the current timestamp. * @type {function} */ static get now() { return now; } /** * Set the callback for returning the current timestamp. * The function should return a number, which will be interpreted as an Epoch millisecond count * @type {function} * @example Settings.now = () => Date.now() + 3000 // pretend it is 3 seconds in the future * @example Settings.now = () => 0 // always pretend it's Jan 1, 1970 at midnight in UTC time */ static set now(n) { now = n; } /** * Get the default time zone to create DateTimes in. * @type {string} */ static get defaultZoneName() { return Settings.defaultZone.name; } /** * Set the default time zone to create DateTimes in. Does not affect existing instances. * @type {string} */ static set defaultZoneName(z) { if (!z) { defaultZone = null; } else { defaultZone = normalizeZone(z); } } /** * Get the default time zone object to create DateTimes in. Does not affect existing instances. * @type {Zone} */ static get defaultZone() { return defaultZone || LocalZone.instance; } /** * Get the default locale to create DateTimes with. Does not affect existing instances. * @type {string} */ static get defaultLocale() { return defaultLocale; } /** * Set the default locale to create DateTimes with. Does not affect existing instances. * @type {string} */ static set defaultLocale(locale) { defaultLocale = locale; } /** * Get the default numbering system to create DateTimes with. Does not affect existing instances. * @type {string} */ static get defaultNumberingSystem() { return defaultNumberingSystem; } /** * Set the default numbering system to create DateTimes with. Does not affect existing instances. * @type {string} */ static set defaultNumberingSystem(numberingSystem) { defaultNumberingSystem = numberingSystem; } /** * Get the default output calendar to create DateTimes with. Does not affect existing instances. * @type {string} */ static get defaultOutputCalendar() { return defaultOutputCalendar; } /** * Set the default output calendar to create DateTimes with. Does not affect existing instances. * @type {string} */ static set defaultOutputCalendar(outputCalendar) { defaultOutputCalendar = outputCalendar; } /** * Get whether Luxon will throw when it encounters invalid DateTimes, Durations, or Intervals * @type {boolean} */ static get throwOnInvalid() { return throwOnInvalid; } /** * Set whether Luxon will throw when it encounters invalid DateTimes, Durations, or Intervals * @type {boolean} */ static set throwOnInvalid(t) { throwOnInvalid = t; } /** * Reset Luxon's global caches. Should only be necessary in testing scenarios. * @return {void} */ static resetCaches() { Locale.resetCache(); IANAZone.resetCache(); } } function stringifyTokens(splits, tokenToString) { let s = ""; for (const token of splits) { if (token.literal) { s += token.val; } else { s += tokenToString(token.val); } } return s; } const tokenToObject = { D: DATE_SHORT, DD: DATE_MED, DDD: DATE_FULL, DDDD: DATE_HUGE, t: TIME_SIMPLE, tt: TIME_WITH_SECONDS, ttt: TIME_WITH_SHORT_OFFSET, tttt: TIME_WITH_LONG_OFFSET, T: TIME_24_SIMPLE, TT: TIME_24_WITH_SECONDS, TTT: TIME_24_WITH_SHORT_OFFSET, TTTT: TIME_24_WITH_LONG_OFFSET, f: DATETIME_SHORT, ff: DATETIME_MED, fff: DATETIME_FULL, ffff: DATETIME_HUGE, F: DATETIME_SHORT_WITH_SECONDS, FF: DATETIME_MED_WITH_SECONDS, FFF: DATETIME_FULL_WITH_SECONDS, FFFF: DATETIME_HUGE_WITH_SECONDS }; /** * @private */ class Formatter { static create(locale, opts = {}) { return new Formatter(locale, opts); } static parseFormat(fmt) { let current = null, currentFull = "", bracketed = false; const splits = []; for (let i = 0; i < fmt.length; i++) { const c = fmt.charAt(i); if (c === "'") { if (currentFull.length > 0) { splits.push({ literal: bracketed, val: currentFull }); } current = null; currentFull = ""; bracketed = !bracketed; } else if (bracketed) { currentFull += c; } else if (c === current) { currentFull += c; } else { if (currentFull.length > 0) { splits.push({ literal: false, val: currentFull }); } currentFull = c; current = c; } } if (currentFull.length > 0) { splits.push({ literal: bracketed, val: currentFull }); } return splits; } constructor(locale, formatOpts) { this.opts = formatOpts; this.loc = locale; this.systemLoc = null; } formatWithSystemDefault(dt, opts) { if (this.systemLoc === null) { this.systemLoc = this.loc.redefaultToSystem(); } const df = this.systemLoc.dtFormatter(dt, Object.assign({}, this.opts, opts)); return df.format(); } formatDateTime(dt, opts = {}) { const df = this.loc.dtFormatter(dt, Object.assign({}, this.opts, opts)); return df.format(); } formatDateTimeParts(dt, opts = {}) { const df = this.loc.dtFormatter(dt, Object.assign({}, this.opts, opts)); return df.formatToParts(); } resolvedOptions(dt, opts = {}) { const df = this.loc.dtFormatter(dt, Object.assign({}, this.opts, opts)); return df.resolvedOptions(); } num(n, p = 0) { // we get some perf out of doing this here, annoyingly if (this.opts.forceSimple) { return padStart(n, p); } const opts = Object.assign({}, this.opts); if (p > 0) { opts.padTo = p; } return this.loc.numberFormatter(opts).format(n); } formatDateTimeFromString(dt, fmt) { const knownEnglish = this.loc.listingMode() === "en"; const string = (opts, extract) => this.loc.extract(dt, opts, extract), formatOffset = opts => { if (dt.isOffsetFixed && dt.offset === 0 && opts.allowZ) { return "Z"; } const hours = Math.trunc(dt.offset / 60), minutes = Math.abs(dt.offset % 60), sign = hours >= 0 ? "+" : "-", base = `${sign}${Math.abs(hours)}`; switch (opts.format) { case "short": return `${sign}${this.num(Math.abs(hours), 2)}:${this.num(minutes, 2)}`; case "narrow": return minutes > 0 ? `${base}:${minutes}` : base; case "techie": return `${sign}${this.num(Math.abs(hours), 2)}${this.num(minutes, 2)}`; default: throw new RangeError(`Value format ${opts.format} is out of range for property format`); } }, meridiem = () => knownEnglish ? meridiemForDateTime(dt) : string({ hour: "numeric", hour12: true }, "dayperiod"), month = (length, standalone) => knownEnglish ? monthForDateTime(dt, length) : string(standalone ? { month: length } : { month: length, day: "numeric" }, "month"), weekday = (length, standalone) => knownEnglish ? weekdayForDateTime(dt, length) : string(standalone ? { weekday: length } : { weekday: length, month: "long", day: "numeric" }, "weekday"), maybeMacro = token => { const macro = tokenToObject[token]; if (macro) { return this.formatWithSystemDefault(dt, macro); } else { return token; } }, era = length => knownEnglish ? eraForDateTime(dt, length) : string({ era: length }, "era"), tokenToString = token => { const outputCal = this.loc.outputCalendar; // Where possible: http://cldr.unicode.org/translation/date-time#TOC-Stand-Alone-vs.-Format-Styles switch (token) { // ms case "S": return this.num(dt.millisecond); case "u": // falls through case "SSS": return this.num(dt.millisecond, 3); // seconds case "s": return this.num(dt.second); case "ss": return this.num(dt.second, 2); // minutes case "m": return this.num(dt.minute); case "mm": return this.num(dt.minute, 2); // hours case "h": return this.num(dt.hour % 12 === 0 ? 12 : dt.hour % 12); case "hh": return this.num(dt.hour % 12 === 0 ? 12 : dt.hour % 12, 2); case "H": return this.num(dt.hour); case "HH": return this.num(dt.hour, 2); // offset case "Z": // like +6 return formatOffset({ format: "narrow", allowZ: this.opts.allowZ }); case "ZZ": // like +06:00 return formatOffset({ format: "short", allowZ: this.opts.allowZ }); case "ZZZ": // like +0600 return formatOffset({ format: "techie", allowZ: false }); case "ZZZZ": // like EST return dt.offsetNameShort; case "ZZZZZ": // like Eastern Standard Time return dt.offsetNameLong; // zone case "z": // like America/New_York return dt.zoneName; // meridiems case "a": return meridiem(); // dates case "d": return outputCal ? string({ day: "numeric" }, "day") : this.num(dt.day); case "dd": return outputCal ? string({ day: "2-digit" }, "day") : this.num(dt.day, 2); // weekdays - standalone case "c": // like 1 return this.num(dt.weekday); case "ccc": // like 'Tues' return weekday("short", true); case "cccc": // like 'Tuesday' return weekday("long", true); case "ccccc": // like 'T' return weekday("narrow", true); // weekdays - format case "E": // like 1 return this.num(dt.weekday); case "EEE": // like 'Tues' return weekday("short", false); case "EEEE": // like 'Tuesday' return weekday("long", false); case "EEEEE": // like 'T' return weekday("narrow", false); // months - standalone case "L": // like 1 return outputCal ? string({ month: "numeric", day: "numeric" }, "month") : this.num(dt.month); case "LL": // like 01, doesn't seem to work return outputCal ? string({ month: "2-digit", day: "numeric" }, "month") : this.num(dt.month, 2); case "LLL": // like Jan return month("short", true); case "LLLL": // like January return month("long", true); case "LLLLL": // like J return month("narrow", true); // months - format case "M": // like 1 return outputCal ? string({ month: "numeric" }, "month") : this.num(dt.month); case "MM": // like 01 return outputCal ? string({ month: "2-digit" }, "month") : this.num(dt.month, 2); case "MMM": // like Jan return month("short", false); case "MMMM": // like January return month("long", false); case "MMMMM": // like J return month("narrow", false); // years case "y": // like 2014 return outputCal ? string({ year: "numeric" }, "year") : this.num(dt.year); case "yy": // like 14 return outputCal ? string({ year: "2-digit" }, "year") : this.num(dt.year.toString().slice(-2), 2); case "yyyy": // like 0012 return outputCal ? string({ year: "numeric" }, "year") : this.num(dt.year, 4); case "yyyyyy": // like 000012 return outputCal ? string({ year: "numeric" }, "year") : this.num(dt.year, 6); // eras case "G": // like AD return era("short"); case "GG": // like Anno Domini return era("long"); case "GGGGG": return era("narrow"); case "kk": return this.num(dt.weekYear.toString().slice(-2), 2); case "kkkk": return this.num(dt.weekYear, 4); case "W": return this.num(dt.weekNumber); case "WW": return this.num(dt.weekNumber, 2); case "o": return this.num(dt.ordinal); case "ooo": return this.num(dt.ordinal, 3); case "q": // like 1 return this.num(dt.quarter); case "qq": // like 01 return this.num(dt.quarter, 2); case "X": return this.num(Math.floor(dt.ts / 1000)); case "x": return this.num(dt.ts); default: return maybeMacro(token); } }; return stringifyTokens(Formatter.parseFormat(fmt), tokenToString); } formatDurationFromString(dur, fmt) { const tokenToField = token => { switch (token[0]) { case "S": return "millisecond"; case "s": return "second"; case "m": return "minute"; case "h": return "hour"; case "d": return "day"; case "M": return "month"; case "y": return "year"; default: return null; } }, tokenToString = lildur => token => { const mapped = tokenToField(token); if (mapped) { return this.num(lildur.get(mapped), token.length); } else { return token; } }, tokens = Formatter.parseFormat(fmt), realTokens = tokens.reduce((found, { literal, val }) => literal ? found : found.concat(val), []), collapsed = dur.shiftTo(...realTokens.map(tokenToField).filter(t => t)); return stringifyTokens(tokens, tokenToString(collapsed)); } } let intlDTCache = {}; function getCachedDTF(locString, opts = {}) { const key = JSON.stringify([locString, opts]); let dtf = intlDTCache[key]; if (!dtf) { dtf = new Intl.DateTimeFormat(locString, opts); intlDTCache[key] = dtf; } return dtf; } let intlNumCache = {}; function getCachendINF(locString, opts = {}) { const key = JSON.stringify([locString, opts]); let inf = intlNumCache[key]; if (!inf) { inf = new Intl.NumberFormat(locString, opts); intlNumCache[key] = inf; } return inf; } let intlRelCache = {}; function getCachendRTF(locString, opts = {}) { const key = JSON.stringify([locString, opts]); let inf = intlRelCache[key]; if (!inf) { inf = new Intl.RelativeTimeFormat(locString, opts); intlRelCache[key] = inf; } return inf; } let sysLocaleCache = null; function systemLocale() { if (sysLocaleCache) { return sysLocaleCache; } else if (hasIntl()) { const computedSys = new Intl.DateTimeFormat().resolvedOptions().locale; // node sometimes defaults to "und". Override that because that is dumb sysLocaleCache = computedSys === "und" ? "en-US" : computedSys; return sysLocaleCache; } else { sysLocaleCache = "en-US"; return sysLocaleCache; } } function parseLocaleString(localeStr) { // I really want to avoid writing a BCP 47 parser // see, e.g. https://github.com/wooorm/bcp-47 // Instead, we'll do this: // a) if the string has no -u extensions, just leave it alone // b) if it does, use Intl to resolve everything // c) if Intl fails, try again without the -u const uIndex = localeStr.indexOf("-u-"); if (uIndex === -1) { return [localeStr]; } else { let options; const smaller = localeStr.substring(0, uIndex); try { options = getCachedDTF(localeStr).resolvedOptions(); } catch (e) { options = getCachedDTF(smaller).resolvedOptions(); } const { numberingSystem, calendar } = options; // return the smaller one so that we can append the calendar and numbering overrides to it return [smaller, numberingSystem, calendar]; } } function intlConfigString(localeStr, numberingSystem, outputCalendar) { if (hasIntl()) { if (outputCalendar || numberingSystem) { localeStr += "-u"; if (outputCalendar) { localeStr += `-ca-${outputCalendar}`; } if (numberingSystem) { localeStr += `-nu-${numberingSystem}`; } return localeStr; } else { return localeStr; } } else { return []; } } function mapMonths(f) { const ms = []; for (let i = 1; i <= 12; i++) { const dt = DateTime.utc(2016, i, 1); ms.push(f(dt)); } return ms; } function mapWeekdays(f) { const ms = []; for (let i = 1; i <= 7; i++) { const dt = DateTime.utc(2016, 11, 13 + i); ms.push(f(dt)); } return ms; } function listStuff(loc, length, defaultOK, englishFn, intlFn) { const mode = loc.listingMode(defaultOK); if (mode === "error") { return null; } else if (mode === "en") { return englishFn(length); } else { return intlFn(length); } } function supportsFastNumbers(loc) { if (loc.numberingSystem && loc.numberingSystem !== "latn") { return false; } else { return loc.numberingSystem === "latn" || !loc.locale || loc.locale.startsWith("en") || hasIntl() && Intl.DateTimeFormat(loc.intl).resolvedOptions().numberingSystem === "latn"; } } /** * @private */ class PolyNumberFormatter { constructor(intl, forceSimple, opts) { this.padTo = opts.padTo || 0; this.floor = opts.floor || false; if (!forceSimple && hasIntl()) { const intlOpts = { useGrouping: false }; if (opts.padTo > 0) intlOpts.minimumIntegerDigits = opts.padTo; this.inf = getCachendINF(intl, intlOpts); } } format(i) { if (this.inf) { const fixed = this.floor ? Math.floor(i) : i; return this.inf.format(fixed); } else { // to match the browser's numberformatter defaults const fixed = this.floor ? Math.floor(i) : roundTo(i, 3); return padStart(fixed, this.padTo); } } } /** * @private */ class PolyDateFormatter { constructor(dt, intl, opts) { this.opts = opts; this.hasIntl = hasIntl(); let z; if (dt.zone.universal && this.hasIntl) { // Chromium doesn't support fixed-offset zones like Etc/GMT+8 in its formatter, // See https://bugs.chromium.org/p/chromium/issues/detail?id=364374. // So we have to make do. Two cases: // 1. The format options tell us to show the zone. We can't do that, so the best // we can do is format the date in UTC. // 2. The format options don't tell us to show the zone. Then we can adjust them // the time and tell the formatter to show it to us in UTC, so that the time is right // and the bad zone doesn't show up. // We can clean all this up when Chrome fixes this. z = "UTC"; if (opts.timeZoneName) { this.dt = dt; } else { this.dt = dt.offset === 0 ? dt : DateTime.fromMillis(dt.ts + dt.offset * 60 * 1000); } } else if (dt.zone.type === "local") { this.dt = dt; } else { this.dt = dt; z = dt.zone.name; } if (this.hasIntl) { const intlOpts = Object.assign({}, this.opts); if (z) { intlOpts.timeZone = z; } this.dtf = getCachedDTF(intl, intlOpts); } } format() { if (this.hasIntl) { return this.dtf.format(this.dt.toJSDate()); } else { const tokenFormat = formatString(this.opts), loc = Locale.create("en-US"); return Formatter.create(loc).formatDateTimeFromString(this.dt, tokenFormat); } } formatToParts() { if (this.hasIntl && hasFormatToParts()) { return this.dtf.formatToParts(this.dt.toJSDate()); } else { // This is kind of a cop out. We actually could do this for English. However, we couldn't do it for intl strings // and IMO it's too weird to have an uncanny valley like that return []; } } resolvedOptions() { if (this.hasIntl) { return this.dtf.resolvedOptions(); } else { return { locale: "en-US", numberingSystem: "latn", outputCalendar: "gregory" }; } } } /** * @private */ class PolyRelFormatter { constructor(intl, isEnglish, opts) { this.opts = Object.assign({ style: "long" }, opts); if (!isEnglish && hasRelative()) { this.rtf = getCachendRTF(intl, opts); } } format(count, unit) { if (this.rtf) { return this.rtf.format(count, unit); } else { return formatRelativeTime(unit, count, this.opts.numeric, this.opts.style !== "long"); } } formatToParts(count, unit) { if (this.rtf) { return this.rtf.formatToParts(count, unit); } else { return []; } } } /** * @private */ class Locale { static fromOpts(opts) { return Locale.create(opts.locale, opts.numberingSystem, opts.outputCalendar, opts.defaultToEN); } static create(locale, numberingSystem, outputCalendar, defaultToEN = false) { const specifiedLocale = locale || Settings.defaultLocale, // the system locale is useful for human readable strings but annoying for parsing/formatting known formats localeR = specifiedLocale || (defaultToEN ? "en-US" : systemLocale()), numberingSystemR = numberingSystem || Settings.defaultNumberingSystem, outputCalendarR = outputCalendar || Settings.defaultOutputCalendar; return new Locale(localeR, numberingSystemR, outputCalendarR, specifiedLocale); } static resetCache() { sysLocaleCache = null; intlDTCache = {}; intlNumCache = {}; } static fromObject({ locale, numberingSystem, outputCalendar } = {}) { return Locale.create(locale, numberingSystem, outputCalendar); } constructor(locale, numbering, outputCalendar, specifiedLocale) { let [parsedLocale, parsedNumberingSystem, parsedOutputCalendar] = parseLocaleString(locale); this.locale = parsedLocale; this.numberingSystem = numbering || parsedNumberingSystem || null; this.outputCalendar = outputCalendar || parsedOutputCalendar || null; this.intl = intlConfigString(this.locale, this.numberingSystem, this.outputCalendar); this.weekdaysCache = { format: {}, standalone: {} }; this.monthsCache = { format: {}, standalone: {} }; this.meridiemCache = null; this.eraCache = {}; this.specifiedLocale = specifiedLocale; this.fastNumbersCached = null; } get fastNumbers() { if (this.fastNumbersCached == null) { this.fastNumbersCached = supportsFastNumbers(this); } return this.fastNumbersCached; } listingMode(defaultOK = true) { const intl = hasIntl(), hasFTP = intl && hasFormatToParts(), isActuallyEn = this.isEnglish(), hasNoWeirdness = (this.numberingSystem === null || this.numberingSystem === "latn") && (this.outputCalendar === null || this.outputCalendar === "gregory"); if (!hasFTP && !(isActuallyEn && hasNoWeirdness) && !defaultOK) { return "error"; } else if (!hasFTP || isActuallyEn && hasNoWeirdness) { return "en"; } else { return "intl"; } } clone(alts) { if (!alts || Object.getOwnPropertyNames(alts).length === 0) { return this; } else { return Locale.create(alts.locale || this.specifiedLocale, alts.numberingSystem || this.numberingSystem, alts.outputCalendar || this.outputCalendar, alts.defaultToEN || false); } } redefaultToEN(alts = {}) { return this.clone(Object.assign({}, alts, { defaultToEN: true })); } redefaultToSystem(alts = {}) { return this.clone(Object.assign({}, alts, { defaultToEN: false })); } months(length, format = false, defaultOK = true) { return listStuff(this, length, defau