UNPKG

luxon

Version:
1,998 lines (1,749 loc) 256 kB
// 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"); } } /** * @private */ const n = "numeric", s = "short", l = "long"; const DATE_SHORT = { year: n, month: n, day: n, }; const DATE_MED = { year: n, month: s, day: n, }; const DATE_MED_WITH_WEEKDAY = { year: n, month: s, day: n, weekday: s, }; 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: n, }; const TIME_WITH_SECONDS = { hour: n, minute: n, second: n, }; const TIME_WITH_SHORT_OFFSET = { hour: n, minute: n, second: n, timeZoneName: s, }; const TIME_WITH_LONG_OFFSET = { hour: n, minute: n, second: n, timeZoneName: l, }; const TIME_24_SIMPLE = { hour: n, minute: n, hourCycle: "h23", }; const TIME_24_WITH_SECONDS = { hour: n, minute: n, second: n, hourCycle: "h23", }; const TIME_24_WITH_SHORT_OFFSET = { hour: n, minute: n, second: n, hourCycle: "h23", timeZoneName: s, }; const TIME_24_WITH_LONG_OFFSET = { hour: n, minute: n, second: n, hourCycle: "h23", timeZoneName: l, }; const DATETIME_SHORT = { year: n, month: n, day: n, hour: n, minute: n, }; const DATETIME_SHORT_WITH_SECONDS = { year: n, month: n, day: n, hour: n, minute: n, second: n, }; const DATETIME_MED = { year: n, month: s, day: n, hour: n, minute: n, }; const DATETIME_MED_WITH_SECONDS = { year: n, month: s, day: n, hour: n, minute: n, second: n, }; const DATETIME_MED_WITH_WEEKDAY = { year: n, month: s, day: n, weekday: s, hour: n, minute: n, }; const DATETIME_FULL = { year: n, month: l, day: n, hour: n, minute: n, timeZoneName: s, }; const DATETIME_FULL_WITH_SECONDS = { year: n, month: l, day: n, hour: n, minute: n, second: n, timeZoneName: s, }; const DATETIME_HUGE = { year: n, month: l, day: n, weekday: l, hour: n, minute: n, timeZoneName: l, }; const DATETIME_HUGE_WITH_SECONDS = { year: n, month: l, day: n, weekday: l, hour: n, minute: n, second: n, timeZoneName: l, }; /** * @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(); } /** * The IANA name of this zone. * Defaults to `name` if not overwritten by a subclass. * @abstract * @type {string} */ get ianaName() { return this.name; } /** * Returns whether the offset is known to be fixed for the whole year. * @abstract * @type {boolean} */ get isUniversal() { 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(); } /** * Returns the offset's value as a string * @abstract * @param {number} ts - Epoch milliseconds for which to get the offset * @param {string} format - What style of offset to return. * Accepts 'narrow', 'short', or 'techie'. Returning '+6', '+06:00', or '+0600' respectively * @return {string} */ formatOffset(ts, format) { 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$1 = null; /** * Represents the local zone for this JavaScript environment. * @implements {Zone} */ class SystemZone extends Zone { /** * Get a singleton instance of the local zone * @return {SystemZone} */ static get instance() { if (singleton$1 === null) { singleton$1 = new SystemZone(); } return singleton$1; } /** @override **/ get type() { return "system"; } /** @override **/ get name() { return new Intl.DateTimeFormat().resolvedOptions().timeZone; } /** @override **/ get isUniversal() { return false; } /** @override **/ offsetName(ts, { format, locale }) { return parseZoneInfo(ts, format, locale); } /** @override **/ formatOffset(ts, format) { return formatOffset(this.offset(ts), format); } /** @override **/ offset(ts) { return -new Date(ts).getTimezoneOffset(); } /** @override **/ equals(otherZone) { return otherZone.type === "system"; } /** @override **/ get isValid() { return true; } } const dtfCache = new Map(); function makeDTF(zoneName) { let dtf = dtfCache.get(zoneName); if (dtf === undefined) { dtf = new Intl.DateTimeFormat("en-US", { hour12: false, timeZone: zoneName, year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", era: "short", }); dtfCache.set(zoneName, dtf); } return dtf; } const typeToPos = { year: 0, month: 1, day: 2, era: 3, hour: 4, minute: 5, second: 6, }; function hackyOffset(dtf, date) { const formatted = dtf.format(date).replace(/\u200E/g, ""), parsed = /(\d+)\/(\d+)\/(\d+) (AD|BC),? (\d+):(\d+):(\d+)/.exec(formatted), [, fMonth, fDay, fYear, fadOrBc, fHour, fMinute, fSecond] = parsed; return [fYear, fMonth, fDay, fadOrBc, fHour, fMinute, fSecond]; } function partsOffset(dtf, date) { const formatted = dtf.formatToParts(date); const filled = []; for (let i = 0; i < formatted.length; i++) { const { type, value } = formatted[i]; const pos = typeToPos[type]; if (type === "era") { filled[pos] = value; } else if (!isUndefined(pos)) { filled[pos] = parseInt(value, 10); } } return filled; } const ianaZoneCache = new Map(); /** * 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) { let zone = ianaZoneCache.get(name); if (zone === undefined) { ianaZoneCache.set(name, (zone = new IANAZone(name))); } return zone; } /** * Reset local caches. Should only be necessary in testing scenarios. * @return {void} */ static resetCache() { ianaZoneCache.clear(); dtfCache.clear(); } /** * 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("Sport~~blorp") //=> false * @deprecated For backward compatibility, this forwards to isValidZone, better use `isValidZone()` directly instead. * @return {boolean} */ static isValidSpecifier(s) { return this.isValidZone(s); } /** * 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) { if (!zone) { return false; } try { new Intl.DateTimeFormat("en-US", { timeZone: zone }).format(); return true; } catch (e) { return false; } } constructor(name) { super(); /** @private **/ this.zoneName = name; /** @private **/ this.valid = IANAZone.isValidZone(name); } /** * The type of zone. `iana` for all instances of `IANAZone`. * @override * @type {string} */ get type() { return "iana"; } /** * The name of this zone (i.e. the IANA zone name). * @override * @type {string} */ get name() { return this.zoneName; } /** * Returns whether the offset is known to be fixed for the whole year: * Always returns false for all IANA zones. * @override * @type {boolean} */ get isUniversal() { return false; } /** * Returns the offset's common name (such as EST) at the specified timestamp * @override * @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, { format, locale }) { return parseZoneInfo(ts, format, locale, this.name); } /** * Returns the offset's value as a string * @override * @param {number} ts - Epoch milliseconds for which to get the offset * @param {string} format - What style of offset to return. * Accepts 'narrow', 'short', or 'techie'. Returning '+6', '+06:00', or '+0600' respectively * @return {string} */ formatOffset(ts, format) { return formatOffset(this.offset(ts), format); } /** * Return the offset in minutes for this zone at the specified timestamp. * @override * @param {number} ts - Epoch milliseconds for which to compute the offset * @return {number} */ offset(ts) { if (!this.valid) return NaN; const date = new Date(ts); if (isNaN(date)) return NaN; const dtf = makeDTF(this.name); let [year, month, day, adOrBc, hour, minute, second] = dtf.formatToParts ? partsOffset(dtf, date) : hackyOffset(dtf, date); if (adOrBc === "BC") { year = -Math.abs(year) + 1; } // because we're using hour12 and https://bugs.chromium.org/p/chromium/issues/detail?id=1025564&can=2&q=%2224%3A00%22%20datetimeformat const adjustedHour = hour === 24 ? 0 : hour; const asUTC = objToLocalTS({ year, month, day, hour: adjustedHour, minute, second, millisecond: 0, }); let asTS = +date; const over = asTS % 1000; asTS -= over >= 0 ? over : 1000 + over; return (asUTC - asTS) / (60 * 1000); } /** * Return whether this Zone is equal to another zone * @override * @param {Zone} otherZone - the zone to compare * @return {boolean} */ equals(otherZone) { return otherZone.type === "iana" && otherZone.name === this.name; } /** * Return whether this Zone is valid. * @override * @type {boolean} */ get isValid() { return this.valid; } } // todo - remap caching let intlLFCache = {}; function getCachedLF(locString, opts = {}) { const key = JSON.stringify([locString, opts]); let dtf = intlLFCache[key]; if (!dtf) { dtf = new Intl.ListFormat(locString, opts); intlLFCache[key] = dtf; } return dtf; } const intlDTCache = new Map(); function getCachedDTF(locString, opts = {}) { const key = JSON.stringify([locString, opts]); let dtf = intlDTCache.get(key); if (dtf === undefined) { dtf = new Intl.DateTimeFormat(locString, opts); intlDTCache.set(key, dtf); } return dtf; } const intlNumCache = new Map(); function getCachedINF(locString, opts = {}) { const key = JSON.stringify([locString, opts]); let inf = intlNumCache.get(key); if (inf === undefined) { inf = new Intl.NumberFormat(locString, opts); intlNumCache.set(key, inf); } return inf; } const intlRelCache = new Map(); function getCachedRTF(locString, opts = {}) { const { base, ...cacheKeyOpts } = opts; // exclude `base` from the options const key = JSON.stringify([locString, cacheKeyOpts]); let inf = intlRelCache.get(key); if (inf === undefined) { inf = new Intl.RelativeTimeFormat(locString, opts); intlRelCache.set(key, inf); } return inf; } let sysLocaleCache = null; function systemLocale() { if (sysLocaleCache) { return sysLocaleCache; } else { sysLocaleCache = new Intl.DateTimeFormat().resolvedOptions().locale; return sysLocaleCache; } } const intlResolvedOptionsCache = new Map(); function getCachedIntResolvedOptions(locString) { let opts = intlResolvedOptionsCache.get(locString); if (opts === undefined) { opts = new Intl.DateTimeFormat(locString).resolvedOptions(); intlResolvedOptionsCache.set(locString, opts); } return opts; } const weekInfoCache = new Map(); function getCachedWeekInfo(locString) { let data = weekInfoCache.get(locString); if (!data) { const locale = new Intl.Locale(locString); // browsers currently implement this as a property, but spec says it should be a getter function data = "getWeekInfo" in locale ? locale.getWeekInfo() : locale.weekInfo; // minimalDays was removed from WeekInfo: https://github.com/tc39/proposal-intl-locale-info/issues/86 if (!("minimalDays" in data)) { data = { ...fallbackWeekSettings, ...data }; } weekInfoCache.set(locString, data); } return data; } 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 // private subtags and unicode subtags have ordering requirements, // and we're not properly parsing this, so just strip out the // private ones if they exist. const xIndex = localeStr.indexOf("-x-"); if (xIndex !== -1) { localeStr = localeStr.substring(0, xIndex); } const uIndex = localeStr.indexOf("-u-"); if (uIndex === -1) { return [localeStr]; } else { let options; let selectedStr; try { options = getCachedDTF(localeStr).resolvedOptions(); selectedStr = localeStr; } catch (e) { const smaller = localeStr.substring(0, uIndex); options = getCachedDTF(smaller).resolvedOptions(); selectedStr = smaller; } const { numberingSystem, calendar } = options; return [selectedStr, numberingSystem, calendar]; } } function intlConfigString(localeStr, numberingSystem, outputCalendar) { if (outputCalendar || numberingSystem) { if (!localeStr.includes("-u-")) { localeStr += "-u"; } if (outputCalendar) { localeStr += `-ca-${outputCalendar}`; } if (numberingSystem) { localeStr += `-nu-${numberingSystem}`; } return localeStr; } else { return localeStr; } } function mapMonths(f) { const ms = []; for (let i = 1; i <= 12; i++) { const dt = DateTime.utc(2009, 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, englishFn, intlFn) { const mode = loc.listingMode(); 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") || getCachedIntResolvedOptions(loc.locale).numberingSystem === "latn" ); } } /** * @private */ class PolyNumberFormatter { constructor(intl, forceSimple, opts) { this.padTo = opts.padTo || 0; this.floor = opts.floor || false; const { padTo, floor, ...otherOpts } = opts; if (!forceSimple || Object.keys(otherOpts).length > 0) { const intlOpts = { useGrouping: false, ...opts }; if (opts.padTo > 0) intlOpts.minimumIntegerDigits = opts.padTo; this.inf = getCachedINF(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.originalZone = undefined; let z = undefined; if (this.opts.timeZone) { // Don't apply any workarounds if a timeZone is explicitly provided in opts this.dt = dt; } else if (dt.zone.type === "fixed") { // UTC-8 or Etc/UTC-8 are not part of tzdata, only Etc/GMT+8 and the like. // That is why fixed-offset TZ is set to that unless it is: // 1. Representing offset 0 when UTC is used to maintain previous behavior and does not become GMT. // 2. Unsupported by the browser: // - some do not support Etc/ // - < Etc/GMT-14, > Etc/GMT+12, and 30-minute or 45-minute offsets are not part of tzdata const gmtOffset = -1 * (dt.offset / 60); const offsetZ = gmtOffset >= 0 ? `Etc/GMT+${gmtOffset}` : `Etc/GMT${gmtOffset}`; if (dt.offset !== 0 && IANAZone.create(offsetZ).valid) { z = offsetZ; this.dt = dt; } else { // Not all fixed-offset zones like Etc/+4:30 are present in tzdata so // we manually apply the offset and substitute the zone as needed. z = "UTC"; this.dt = dt.offset === 0 ? dt : dt.setZone("UTC").plus({ minutes: dt.offset }); this.originalZone = dt.zone; } } else if (dt.zone.type === "system") { this.dt = dt; } else if (dt.zone.type === "iana") { this.dt = dt; z = dt.zone.name; } else { // Custom zones can have any offset / offsetName so we just manually // apply the offset and substitute the zone as needed. z = "UTC"; this.dt = dt.setZone("UTC").plus({ minutes: dt.offset }); this.originalZone = dt.zone; } const intlOpts = { ...this.opts }; intlOpts.timeZone = intlOpts.timeZone || z; this.dtf = getCachedDTF(intl, intlOpts); } format() { if (this.originalZone) { // If we have to substitute in the actual zone name, we have to use // formatToParts so that the timezone can be replaced. return this.formatToParts() .map(({ value }) => value) .join(""); } return this.dtf.format(this.dt.toJSDate()); } formatToParts() { const parts = this.dtf.formatToParts(this.dt.toJSDate()); if (this.originalZone) { return parts.map((part) => { if (part.type === "timeZoneName") { const offsetName = this.originalZone.offsetName(this.dt.ts, { locale: this.dt.locale, format: this.opts.timeZoneName, }); return { ...part, value: offsetName, }; } else { return part; } }); } return parts; } resolvedOptions() { return this.dtf.resolvedOptions(); } } /** * @private */ class PolyRelFormatter { constructor(intl, isEnglish, opts) { this.opts = { style: "long", ...opts }; if (!isEnglish && hasRelative()) { this.rtf = getCachedRTF(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 []; } } } const fallbackWeekSettings = { firstDay: 1, minimalDays: 4, weekend: [6, 7], }; /** * @private */ class Locale { static fromOpts(opts) { return Locale.create( opts.locale, opts.numberingSystem, opts.outputCalendar, opts.weekSettings, opts.defaultToEN ); } static create(locale, numberingSystem, outputCalendar, weekSettings, defaultToEN = false) { const specifiedLocale = locale || Settings.defaultLocale; // the system locale is useful for human-readable strings but annoying for parsing/formatting known formats const localeR = specifiedLocale || (defaultToEN ? "en-US" : systemLocale()); const numberingSystemR = numberingSystem || Settings.defaultNumberingSystem; const outputCalendarR = outputCalendar || Settings.defaultOutputCalendar; const weekSettingsR = validateWeekSettings(weekSettings) || Settings.defaultWeekSettings; return new Locale(localeR, numberingSystemR, outputCalendarR, weekSettingsR, specifiedLocale); } static resetCache() { sysLocaleCache = null; intlDTCache.clear(); intlNumCache.clear(); intlRelCache.clear(); intlResolvedOptionsCache.clear(); weekInfoCache.clear(); } static fromObject({ locale, numberingSystem, outputCalendar, weekSettings } = {}) { return Locale.create(locale, numberingSystem, outputCalendar, weekSettings); } constructor(locale, numbering, outputCalendar, weekSettings, specifiedLocale) { const [parsedLocale, parsedNumberingSystem, parsedOutputCalendar] = parseLocaleString(locale); this.locale = parsedLocale; this.numberingSystem = numbering || parsedNumberingSystem || null; this.outputCalendar = outputCalendar || parsedOutputCalendar || null; this.weekSettings = weekSettings; 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() { const isActuallyEn = this.isEnglish(); const hasNoWeirdness = (this.numberingSystem === null || this.numberingSystem === "latn") && (this.outputCalendar === null || this.outputCalendar === "gregory"); return isActuallyEn && hasNoWeirdness ? "en" : "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, validateWeekSettings(alts.weekSettings) || this.weekSettings, alts.defaultToEN || false ); } } redefaultToEN(alts = {}) { return this.clone({ ...alts, defaultToEN: true }); } redefaultToSystem(alts = {}) { return this.clone({ ...alts, defaultToEN: false }); } months(length, format = false) { return listStuff(this, length, months, () => { const intl = format ? { month: length, day: "numeric" } : { month: length }, formatStr = format ? "format" : "standalone"; if (!this.monthsCache[formatStr][length]) { this.monthsCache[formatStr][length] = mapMonths((dt) => this.extract(dt, intl, "month")); } return this.monthsCache[formatStr][length]; }); } weekdays(length, format = false) { return listStuff(this, length, weekdays, () => { const intl = format ? { weekday: length, year: "numeric", month: "long", day: "numeric" } : { weekday: length }, formatStr = format ? "format" : "standalone"; if (!this.weekdaysCache[formatStr][length]) { this.weekdaysCache[formatStr][length] = mapWeekdays((dt) => this.extract(dt, intl, "weekday") ); } return this.weekdaysCache[formatStr][length]; }); } meridiems() { return listStuff( this, undefined, () => meridiems, () => { // In theory there could be aribitrary day periods. We're gonna assume there are exactly two // for AM and PM. This is probably wrong, but it's makes parsing way easier. if (!this.meridiemCache) { const intl = { hour: "numeric", hourCycle: "h12" }; this.meridiemCache = [DateTime.utc(2016, 11, 13, 9), DateTime.utc(2016, 11, 13, 19)].map( (dt) => this.extract(dt, intl, "dayperiod") ); } return this.meridiemCache; } ); } eras(length) { return listStuff(this, length, eras, () => { const intl = { era: length }; // This is problematic. Different calendars are going to define eras totally differently. What I need is the minimum set of dates // to definitely enumerate them. if (!this.eraCache[length]) { this.eraCache[length] = [DateTime.utc(-40, 1, 1), DateTime.utc(2017, 1, 1)].map((dt) => this.extract(dt, intl, "era") ); } return this.eraCache[length]; }); } extract(dt, intlOpts, field) { const df = this.dtFormatter(dt, intlOpts), results = df.formatToParts(), matching = results.find((m) => m.type.toLowerCase() === field); return matching ? matching.value : null; } numberFormatter(opts = {}) { // this forcesimple option is never used (the only caller short-circuits on it, but it seems safer to leave) // (in contrast, the rest of the condition is used heavily) return new PolyNumberFormatter(this.intl, opts.forceSimple || this.fastNumbers, opts); } dtFormatter(dt, intlOpts = {}) { return new PolyDateFormatter(dt, this.intl, intlOpts); } relFormatter(opts = {}) { return new PolyRelFormatter(this.intl, this.isEnglish(), opts); } listFormatter(opts = {}) { return getCachedLF(this.intl, opts); } isEnglish() { return ( this.locale === "en" || this.locale.toLowerCase() === "en-us" || getCachedIntResolvedOptions(this.intl).locale.startsWith("en-us") ); } getWeekSettings() { if (this.weekSettings) { return this.weekSettings; } else if (!hasLocaleWeekInfo()) { return fallbackWeekSettings; } else { return getCachedWeekInfo(this.locale); } } getStartOfWeek() { return this.getWeekSettings().firstDay; } getMinDaysInFirstWeek() { return this.getWeekSettings().minimalDays; } getWeekendDays() { return this.getWeekSettings().weekend; } equals(other) { return ( this.locale === other.locale && this.numberingSystem === other.numberingSystem && this.outputCalendar === other.outputCalendar ); } toString() { return `Locale(${this.locale}, ${this.numberingSystem}, ${this.outputCalendar})`; } } let singleton = null; /** * A zone with a fixed offset (meaning no DST) * @implements {Zone} */ class FixedOffsetZone extends Zone { /** * Get a singleton instance of UTC * @return {FixedOffsetZone} */ static get utcInstance() { if (singleton === null) { singleton = new FixedOffsetZone(0); } return singleton; } /** * 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; } /** * The type of zone. `fixed` for all instances of `FixedOffsetZone`. * @override * @type {string} */ get type() { return "fixed"; } /** * The name of this zone. * All fixed zones' names always start with "UTC" (plus optional offset) * @override * @type {string} */ get name() { return this.fixed === 0 ? "UTC" : `UTC${formatOffset(this.fixed, "narrow")}`; } /** * The IANA name of this zone, i.e. `Etc/UTC` or `Etc/GMT+/-nn` * * @override * @type {string} */ get ianaName() { if (this.fixed === 0) { return "Etc/UTC"; } else { return `Etc/GMT${formatOffset(-this.fixed, "narrow")}`; } } /** * Returns the offset's common name at the specified timestamp. * * For fixed offset zones this equals to the zone name. * @override */ offsetName() { return this.name; } /** * Returns the offset's value as a string * @override * @param {number} ts - Epoch milliseconds for which to get the offset * @param {string} format - What style of offset to return. * Accepts 'narrow', 'short', or 'techie'. Returning '+6', '+06:00', or '+0600' respectively * @return {string} */ formatOffset(ts, format) { return formatOffset(this.fixed, format); } /** * Returns whether the offset is known to be fixed for the whole year: * Always returns true for all fixed offset zones. * @override * @type {boolean} */ get isUniversal() { return true; } /** * Return the offset in minutes for this zone at the specified timestamp. * * For fixed offset zones, this is constant and does not depend on a timestamp. * @override * @return {number} */ offset() { return this.fixed; } /** * Return whether this Zone is equal to another zone (i.e. also fixed and same offset) * @override * @param {Zone} otherZone - the zone to compare * @return {boolean} */ equals(otherZone) { return otherZone.type === "fixed" && otherZone.fixed === this.fixed; } /** * Return whether this Zone is valid: * All fixed offset zones are valid. * @override * @type {boolean} */ 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 isUniversal() { return false; } /** @override **/ offsetName() { return null; } /** @override **/ formatOffset() { return ""; } /** @override **/ offset() { return NaN; } /** @override **/ equals() { return false; } /** @override **/ get isValid() { return false; } } /** * @private */ function normalizeZone(input, defaultZone) { if (isUndefined(input) || input === null) { return defaultZone; } else if (input instanceof Zone) { return input; } else if (isString(input)) { const lowered = input.toLowerCase(); if (lowered === "default") return defaultZone; else if (lowered === "local" || lowered === "system") return SystemZone.instance; else if (lowered === "utc" || lowered === "gmt") return FixedOffsetZone.utcInstance; else return FixedOffsetZone.parseSpecifier(lowered) || IANAZone.create(input); } else if (isNumber(input)) { return FixedOffsetZone.instance(input); } else if (typeof input === "object" && "offset" in input && typeof input.offset === "function") { // 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); } } const numberingSystems = { arab: "[\u0660-\u0669]", arabext: "[\u06F0-\u06F9]", bali: "[\u1B50-\u1B59]", beng: "[\u09E6-\u09EF]", deva: "[\u0966-\u096F]", fullwide: "[\uFF10-\uFF19]", gujr: "[\u0AE6-\u0AEF]", hanidec: "[〇|一|二|三|四|五|六|七|八|九]", khmr: "[\u17E0-\u17E9]", knda: "[\u0CE6-\u0CEF]", laoo: "[\u0ED0-\u0ED9]", limb: "[\u1946-\u194F]", mlym: "[\u0D66-\u0D6F]", mong: "[\u1810-\u1819]", mymr: "[\u1040-\u1049]", orya: "[\u0B66-\u0B6F]", tamldec: "[\u0BE6-\u0BEF]", telu: "[\u0C66-\u0C6F]", thai: "[\u0E50-\u0E59]", tibt: "[\u0F20-\u0F29]", latn: "\\d", }; const numberingSystemsUTF16 = { arab: [1632, 1641], arabext: [1776, 1785], bali: [6992, 7001], beng: [2534, 2543], deva: [2406, 2415], fullwide: [65296, 65303], gujr: [2790, 2799], khmr: [6112, 6121], knda: [3302, 3311], laoo: [3792, 3801], limb: [6470, 6479], mlym: [3430, 3439], mong: [6160, 6169], mymr: [4160, 4169], orya: [2918, 2927], tamldec: [3046, 3055], telu: [3174, 3183], thai: [3664, 3673], tibt: [3872, 3881], }; const hanidecChars = numberingSystems.hanidec.replace(/[\[|\]]/g, "").split(""); function parseDigits(str) { let value = parseInt(str, 10); if (isNaN(value)) { value = ""; for (let i = 0; i < str.length; i++) { const code = str.charCodeAt(i); if (str[i].search(numberingSystems.hanidec) !== -1) { value += hanidecChars.indexOf(str[i]); } else { for (const key in numberingSystemsUTF16) { const [min, max] = numberingSystemsUTF16[key]; if (code >= min && code <= max) { value += code - min; } } } } return parseInt(value, 10); } else { return value; } } // cache of {numberingSystem: {append: regex}} const digitRegexCache = new Map(); function resetDigitRegexCache() { digitRegexCache.clear(); } function digitRegex({ numberingSystem }, append = "") { const ns = numberingSystem || "latn"; let appendCache = digitRegexCache.get(ns); if (appendCache === undefined) { appendCache = new Map(); digitRegexCache.set(ns, appendCache); } let regex = appendCache.get(append); if (regex === undefined) { regex = new RegExp(`${numberingSystems[ns]}${append}`); appendCache.set(append, regex); } return regex; } let now = () => Date.now(), defaultZone = "system", defaultLocale = null, defaultNumberingSystem = null, defaultOutputCalendar = null, twoDigitCutoffYear = 60, throwOnInvalid, defaultWeekSettings = null; /** * 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; } /** * Set the default time zone to create DateTimes in. Does not affect existing instances. * Use the value "system" to reset this value to the system's time zone. * @type {string} */ static set defaultZone(zone) { defaultZone = zone; } /** * Get the default time zone object currently used to create DateTimes. Does not affect existing instances. * The default value is the system's time zone (the one set on the machine that runs this code). * @type {Zone} */ static get defaultZone() { return normalizeZone(defaultZone, SystemZone.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; } /** * @typedef {Object} WeekSettings * @property {number} firstDay * @property {number} minimalDays * @property {number[]} weekend */ /** * @return {WeekSettings|null} */ static get defaultWeekSettings() { return defaultWeekSettings; } /** * Allows overriding the default locale week settings, i.e. the start of the week, the weekend and * how many days are required in the first week of a year. * Does not affect existing instances. * * @param {WeekSettings|null} weekSettings */ static set defaultWeekSettings(weekSettings) { defaultWeekSettings = validateWeekSettings(weekSettings); } /** * Get the cutoff year for whether a 2-digit year string is interpreted in the current or previous century. Numbers higher than the cutoff will be considered to mean 19xx and numbers lower or equal to the cutoff will be considered 20xx. * @type {number} */ static get twoDigitCutoffYear() { return twoDigitCutoffYear; } /** * Set the cutoff year for whether a 2-digit year string is interpreted in the current or previous century. Numbers higher than the cutoff will be considered to mean 19xx and numbers lower or equal to the cutoff will be considered 20xx. * @type {number} * @example Settings.twoDigitCutoffYear = 0 // all 'yy' are interpreted as 20th century * @example Settings.twoDigitCutoffYear = 99 // all 'yy' are interpreted as 21st century * @example Settings.twoDigitCutoffYear = 50 // '49' -> 2049; '50' -> 1950 * @example Settings.twoDigitCutoffYear = 1950 // interpreted as 50 * @example Settings.twoDigitCutoffYear = 2050 // ALSO interpreted as 50 */ static set twoDigitCutoffYear(cutoffYear) { twoDigitCutoffYear = cutoffYear % 100; } /** * 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(); DateTime.resetCache(); resetDigitRegexCache(); } } class Invalid { constructor(reason, explanation) { this.reason = reason; this.explanation = explanation; } toMessage() { if (this.explanation) { return `${this.reason}: ${this.explanation}`; } else { return this.reason; } } } const nonLeapLadder = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334], leapLadder = [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335]; function unitOutOfRange(unit, value) { return new Invalid( "unit out of range", `you specified ${value} (of type ${typeof value}) as a ${unit}, which is invalid` ); } function dayOfWeek(year, month, day) { const d = new Date(Date.UTC(year, month - 1, day)); if (year < 100 && year >= 0) { d.setUTCFullYear(d.getUTCFullYear() - 1900); } const js = d.getUTCDay(); return js === 0 ? 7 : js; } function computeOrdinal(year, month, day) { return day + (isLeapYear(year) ? leapLadder : nonLeapLadder)[month - 1]; } function uncomputeOrdinal(year, ordinal) { const table = isLeapYear(year) ? leapLadder : nonLeapLadder, month0 = table.findIndex((i) => i < ordinal), day = ordinal - table[month0]; return { month: month0 + 1, day }; } function isoWeekdayToLocal(isoWeekday, startOfWeek) { return ((isoWeekday - startOfWeek + 7) % 7) + 1; } /** * @private */ function gregorianToWeek(gregObj, minDaysInFirstWeek = 4, startOfWeek = 1) { const { year, month, day } = gregObj, ordinal = computeOrdinal(year, month, day), weekday = isoWeekdayToLocal(dayOfWeek(year, month, day), startOfWeek); let weekNumber = Math.floor((ordinal - weekday + 14 - minDaysInFirstWeek) / 7), weekYear; if (weekNumber < 1) { weekYear = year - 1; weekNumber = weeksInWeekYear(weekYear, minDaysInFirstWeek, startOfWeek); } else if (weekNumber > weeksInWeekYear(year, minDaysInFirstWeek, startOfWeek)) { weekYear = year + 1; weekNumber = 1; } else { weekYear = year; } return { weekYear, weekNumber, weekday, ...timeObject(gregObj) }; } function weekToGregorian(weekData, minDaysInFirstWeek = 4, startOfWeek = 1) { const { weekYear, weekNumber, weekday } = weekData, weekdayOfJan4 = isoWeekdayToLocal(dayOfWeek(weekYear, 1, minDaysInFirstWeek), startOfWeek), yearInDays = daysInYear(weekYear); let ordinal = weekNumber * 7 + weekday - weekdayOfJan4 - 7 + minDaysInFirstWeek, year; if (ordinal < 1) { year = weekYear - 1; ordinal += daysInYear(year); } else if (ordinal > yearInDays) { year = weekYear + 1; ordinal -= daysInYear(weekYear); } else { year = weekYear; } const { month, day } = uncomputeOrdinal(year, ordinal); return { year, month, day, ...timeObject(weekData) }; } function gregorianToOrdinal(gregData) { const { year, month, day } = gregData; const ordinal = computeOrdinal(year, month, day); return { year, ordinal, ...timeObject(gregData) }; } function ordinalToGregorian(ordinalData) { const { year, ordinal } = ordinalData; const { month, day } = uncomputeOrdinal(year, ordinal); return { year, month, day, ...timeObject(ordinalData) }; } /** * Check if local week units like localWeekday are used in obj. * If so, validates that they are not mixed with ISO week units and then copies them to the normal week unit properties. * Modifies obj in-place! * @param obj the object values */ function usesLocalWeekValues(obj, loc) { const hasLocaleWeekData = !isUndefined(obj.localWeekday) || !isUndefined(obj.localWeekNumber) || !isUndefined(obj.localWeekYear); if (hasLocaleWeekData) { const hasIsoWeekData = !isUndefined(obj.weekday) || !isUndefined(obj.weekNumber) || !isUndefined(obj.weekYear); if (hasIsoWeekData) { throw new ConflictingSpecificationError( "Cannot mix locale-based week fields with ISO-based week fields" ); } if (!isUndefined(obj.localWeekday)) obj.weekday = obj.localWeekday; if (!isUndefined(obj.localWeekNumber)) obj.weekNumber = obj.localWeekNumber; if (!isUndefined(obj.localWeekYear)) obj.weekYear = obj.localWeekYear; delete obj.localWeekday; delete obj.localWeekNumber; delete obj.localWeekYear; return { minDaysInFirstWeek: loc.getMinDaysInFirstWeek(), startOfWeek: loc.getStartOfWeek(), }; } else { return { minDaysInFirstWeek: 4, startOfWeek: 1 }; } } function hasInvalidWeekData(obj, minDaysInFirstWeek = 4, startOfWeek = 1) { const validYear = isInteger(obj.weekYear), validWeek = integerBetween( obj.weekNumber, 1, weeksInWeekYear(obj.weekYear, minDaysInFirstWeek, startOfWeek) ), validWeekday = integerBetween(obj.weekday, 1, 7); if (!validYear) { return unitOutOfRange("weekYear", obj.weekYear); } else if (!validWeek) { return unitOutOfRange("week", obj.weekNumber); } else if (!validWeekday) { return unitOutOfRange("weekday", obj.weekday); } else return false; } function hasInvalidOrdinalData(obj) { const validYear = isInteger(obj.year), validOrdinal = integerBetween(obj.ordinal, 1, daysInYear(obj.year)); if (!validYear) { return unitOutOfRange("year", obj.year); } else if (!validOrdinal) { return unitOutOfRange("ordinal", obj.ordinal); } else return false; } function hasInvalidGregorianData(obj) { const validYear = isInteger(obj.year), validMonth = integerBetween(obj.month, 1, 12), validDay = integerBetween(obj.day, 1, daysInMonth(obj.year, obj.month)); if (!validYear) { return unitOutOfRange("year", obj.year); } else if (!validMonth) { return unitOutOfRange("month", obj.month); } else if (!validDay) { return unitOutOfRange("day", obj.day); } else return false; } function hasInvalidTimeData(obj) { const { hour, minute, second, millisecond } = obj; const validHour = integerBetween(hour, 0, 23) || (hour === 24 && minute === 0 && second === 0 && millisecond === 0), validMinute = integerBetween(minute, 0, 59), validSecond = integerBetween(second, 0, 59), validMillisecond = integerBetween(millisecond, 0, 999); if (!validHour) { return unitOutOfRange("hour", hour); } else if (!validMinute) { return unitOutOfRange("minute", minute); } else if (!validSecond) { return unitOutOfRange("second", second); } else if (!validMillisecond) { return unitOutOfRange("millisecond", millisecond); } else return false; } /* 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 isInteger(o) { return typeof o === "number" && o % 1 === 0; } function isString(o) { return typeof o === "string"; } function isDate(o) { return Object.prototype.toString.call(o) === "[object Date]"; } // CAPABILITIES function hasRelative() { try { return typeof Intl !== "undefined" && !!Intl.RelativeTimeFormat; } catch (e) { return false; } } function hasLocaleWeekInfo() { try { return ( typeof Intl !== "undefined" && !!Intl.Locale && ("weekInfo" in Intl.Locale.prototype || "getWeekInfo" in Intl.Locale.prototype) ); } catch (e) { return false; } } // 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; }, {}); } function hasOwnProperty(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); } function validateWeekSettings(settings) { if (settings == null) { return null; } else if (typeof settings !== "object") { throw new InvalidArgumentError("Week settings must be an object"); } else { if ( !integerBetween(settings.firstDay, 1, 7) || !integerBetween(settings.minimalDays, 1, 7) || !Array.isArray(settings.weekend) || settings.weekend.some((v) => !integerBetween(v, 1, 7)) ) { throw new InvalidArgumentError("Invalid week settings"); } return { firstDay: settings.firstDay, minimalDays: settings.minimalDays, weekend: Array.from(settings.weekend), }; } } // NUMBERS AND STRINGS function integerBetween(thing, bottom, top) { return isInteger(thing) && thing >= bottom && thing <