luxon
Version:
Immutable date wrapper
386 lines (363 loc) • 11.8 kB
JavaScript
import * as English from './english';
import * as Formats from './formats';
import { padStart } from './util';
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: Formats.DATE_SHORT,
DD: Formats.DATE_MED,
DDD: Formats.DATE_FULL,
DDDD: Formats.DATE_HUGE,
t: Formats.TIME_SIMPLE,
tt: Formats.TIME_WITH_SECONDS,
ttt: Formats.TIME_WITH_SHORT_OFFSET,
tttt: Formats.TIME_WITH_LONG_OFFSET,
T: Formats.TIME_24_SIMPLE,
TT: Formats.TIME_24_WITH_SECONDS,
TTT: Formats.TIME_24_WITH_SHORT_OFFSET,
TTTT: Formats.TIME_24_WITH_LONG_OFFSET,
f: Formats.DATETIME_SHORT,
ff: Formats.DATETIME_MED,
fff: Formats.DATETIME_FULL,
ffff: Formats.DATETIME_HUGE,
F: Formats.DATETIME_SHORT_WITH_SECONDS,
FF: Formats.DATETIME_MED_WITH_SECONDS,
FFF: Formats.DATETIME_FULL_WITH_SECONDS,
FFFF: Formats.DATETIME_HUGE_WITH_SECONDS
};
/**
* @private
*/
export default class Formatter {
static create(locale, opts = {}) {
const formatOpts = Object.assign({}, { round: true }, opts);
return new Formatter(locale, formatOpts);
}
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
? English.meridiemForDateTime(dt)
: string({ hour: 'numeric', hour12: true }, 'dayperiod'),
month = (length, standalone) =>
knownEnglish
? English.monthForDateTime(dt, length)
: string(standalone ? { month: length } : { month: length, day: 'numeric' }, 'month'),
weekday = (length, standalone) =>
knownEnglish
? English.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 ? English.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);
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));
}
}