UNPKG

luxon

Version:
314 lines (272 loc) 8.56 kB
import { Util } from './util'; import { English } from './english'; import { DateTime } from '../datetime'; const localeCache = {}; function intlConfigString(locale, numberingSystem, outputCalendar) { let loc = locale || new Intl.DateTimeFormat().resolvedOptions().locale; loc = Array.isArray(locale) ? locale : [locale]; if (outputCalendar || numberingSystem) { loc = loc.map(l => { l += '-u'; if (outputCalendar) { l += '-ca-' + outputCalendar; } if (numberingSystem) { l += '-nu-' + numberingSystem; } return l; }); } return loc; } 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); } } /** * @private */ class PolyNumberFormatter { constructor(opts) { this.padTo = opts.padTo || 0; this.round = opts.round || false; } format(i) { const maybeRounded = this.round ? Math.round(i) : i; return maybeRounded.toString().padStart(this.padTo, '0'); } } class PolyDateFormatter { format(d) { return d.toString(); } resolvedOptions() { return { locale: 'en-US', numberingSystem: 'latn', outputCalendar: 'gregory' }; } } /** * @private */ export class Locale { static fromOpts(opts) { return Locale.create(opts.locale, opts.numberingSystem, opts.outputCalendar); } static create(locale, numberingSystem, outputCalendar) { const localeR = locale || 'en-US', numberingSystemR = numberingSystem || null, outputCalendarR = outputCalendar || null, cacheKey = `${localeR}|${numberingSystemR}|${outputCalendarR}`, cached = localeCache[cacheKey]; if (cached) { return cached; } else { const fresh = new Locale(localeR, numberingSystemR, outputCalendarR); localeCache[cacheKey] = fresh; return fresh; } } static fromObject({ locale, numberingSystem, outputCalendar } = {}) { return Locale.create(locale, numberingSystem, outputCalendar); } constructor(locale, numbering, outputCalendar) { Object.defineProperty(this, 'locale', { value: locale, enumerable: true }); Object.defineProperty(this, 'numberingSystem', { value: numbering || null, enumerable: true }); Object.defineProperty(this, 'outputCalendar', { value: outputCalendar || null, enumerable: true }); Object.defineProperty(this, 'intl', { value: intlConfigString(this.locale, this.numberingSystem, this.outputCalendar), enumerable: false }); // cached usefulness Object.defineProperty(this, 'weekdaysCache', { value: { format: {}, standalone: {} }, enumerable: false }); Object.defineProperty(this, 'monthsCache', { value: { format: {}, standalone: {} }, enumerable: false }); Object.defineProperty(this, 'meridiemCache', { value: null, enumerable: false, writable: true }); Object.defineProperty(this, 'eraCache', { value: {}, enumerable: false, writable: true }); } // todo: cache me listingMode(defaultOk = true) { const hasIntl = Intl && Intl.DateTimeFormat, hasFTP = hasIntl && Intl.DateTimeFormat.prototype.formatToParts, isActuallyEn = this.locale === 'en' || this.locale.toLowerCase() === 'en-us' || (hasIntl && Intl.DateTimeFormat(this.intl) .resolvedOptions() .locale.startsWith('en-US')), 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.locale, alts.numberingSystem || this.numberingSystem, alts.outputCalendar || this.outputCalendar ); } } months(length, format = false, defaultOK = true) { return listStuff(this, length, defaultOK, English.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, defaultOK = true) { return listStuff(this, length, defaultOK, English.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(defaultOK = true) { return listStuff( this, undefined, defaultOK, () => English.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', hour12: true }; 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, defaultOK = true) { return listStuff(this, length, defaultOK, English.eras, () => { const intl = { era: length }; // This is utter bullshit. 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, d] = this.dtFormatter(dt, intlOpts), results = df.formatToParts(d), matching = results.find(m => m.type.toLowerCase() === field); return matching ? matching.value : null; } numberFormatter(opts = {}, intlOpts = {}) { if (Intl && Intl.NumberFormat) { const realIntlOpts = Object.assign({ useGrouping: false }, intlOpts); if (opts.padTo > 0) { realIntlOpts.minimumIntegerDigits = opts.padTo; } if (opts.round) { realIntlOpts.maximumFractionDigits = 0; } return new Intl.NumberFormat(this.intl, realIntlOpts); } else { return new PolyNumberFormatter(opts); } } dtFormatter(dt, intlOpts = {}) { let d, z; if (dt.zone.universal) { // if we have a fixed-offset zone that isn't actually UTC, // (like UTC+8), we need to make do with just displaying // the time in UTC; the formatter doesn't know how to handle UTC+8 d = Util.asIfUTC(dt); z = 'UTC'; } else if (dt.zone.type === 'local') { d = dt.toJSDate(); } else { d = dt.toJSDate(); z = dt.zone.name; } if (Intl && Intl.DateTimeFormat) { const realIntlOpts = Object.assign({}, intlOpts); if (z) { realIntlOpts.timeZone = z; } return [new Intl.DateTimeFormat(this.intl, realIntlOpts), d]; } else { return [new PolyDateFormatter(), d]; } } equals(other) { return ( this.locale === other.locale && this.numberingSystem === other.numberingSystem && this.outputCalendar === other.outputCalendar ); } }