src/impl/tokenParser.js
import { Util } from './util';
import { Formatter } from './formatter';
import { FixedOffsetZone } from '../zones/fixedOffsetZone';
import { IANAZone } from '../zones/IANAZone';
const MISSING_FTP = 'missing Intl.DateTimeFormat.formatToParts support';
function intUnit(regex, post = i => i) {
return { regex, deser: ([s]) => post(parseInt(s, 10)) };
}
function oneOf(strings, startIndex) {
if (strings === null) {
return null;
} else {
return {
regex: RegExp(strings.join('|')),
deser: ([s]) => strings.findIndex(i => s.toLowerCase() === i.toLowerCase()) + startIndex
};
}
}
function offset(regex, groups) {
return { regex, deser: ([, h, m]) => Util.signedOffset(h, m), groups };
}
function simple(regex) {
return { regex, deser: ([s]) => s };
}
function unitForToken(token, loc) {
const one = /\d/,
two = /\d\d/,
three = /\d{3}/,
four = /\d{4}/,
oneOrTwo = /\d\d?/,
oneToThree = /\d\d{2}?/,
twoToFour = /\d\d\d{2}?/,
literal = t => ({ regex: RegExp(t.val), deser: ([s]) => s, literal: true }),
unitate = t => {
if (token.literal) {
return literal(t);
}
switch (t.val) {
// era
case 'G':
return oneOf(loc.eras('short', false), 0);
case 'GG':
return oneOf(loc.eras('long', false), 0);
// years
case 'yyyy':
return intUnit(four);
case 'yy':
return intUnit(twoToFour, Util.untruncateYear);
// months
case 'M':
return intUnit(oneOrTwo);
case 'MM':
return intUnit(two);
case 'MMM':
return oneOf(loc.months('short', false, false), 1);
case 'MMMM':
return oneOf(loc.months('long', false, false), 1);
case 'L':
return intUnit(oneOrTwo);
case 'LL':
return intUnit(two);
case 'LLL':
return oneOf(loc.months('short', true, false), 1);
case 'LLLL':
return oneOf(loc.months('long', true, false), 1);
// dates
case 'd':
return intUnit(oneOrTwo);
case 'dd':
return intUnit(two);
// ordinals
case 'o':
return intUnit(oneToThree);
case 'ooo':
return intUnit(three);
// time
case 'HH':
return intUnit(two);
case 'H':
return intUnit(oneOrTwo);
case 'hh':
return intUnit(two);
case 'h':
return intUnit(oneOrTwo);
case 'mm':
return intUnit(two);
case 'm':
return intUnit(oneOrTwo);
case 's':
return intUnit(oneOrTwo);
case 'ss':
return intUnit(two);
case 'S':
return intUnit(oneToThree);
case 'SSS':
return intUnit(three);
// meridiem
case 'a':
return oneOf(loc.meridiems(), 0);
// weekYear (k)
case 'kkkk':
return intUnit(four);
case 'kk':
return intUnit(twoToFour, Util.untruncateYear);
// weekNumber (W)
case 'W':
return intUnit(oneOrTwo);
case 'WW':
return intUnit(two);
// weekdays
case 'E':
case 'c':
return intUnit(one);
case 'EEE':
return oneOf(loc.weekdays('short', false, false), 1);
case 'EEEE':
return oneOf(loc.weekdays('long', false, false), 1);
case 'ccc':
return oneOf(loc.weekdays('short', true, false), 1);
case 'cccc':
return oneOf(loc.weekdays('long', true, false), 1);
// offset/zone
case 'Z':
case 'ZZ':
return offset(/([+-]\d{1,2})(?::(\d{2}))?/, 2);
case 'ZZZ':
return offset(/([+-]\d{1,2})(\d{2})?/, 2);
// we don't support ZZZZ (PST) or ZZZZZ (Pacific Standard Time) in parsing
// because we don't have any way to figure out what they are
case 'z':
return simple(/[A-Za-z_]+\/[A-Za-z_]+/);
default:
return literal(t);
}
};
const unit = unitate(token) || {
invalidReason: MISSING_FTP
};
unit.token = token;
return unit;
}
function buildRegex(units) {
return [units.map(u => u.regex).reduce((f, r) => `${f}(${r.source})`, ''), units];
}
function match(input, regex, handlers) {
const matches = input.match(regex);
if (matches) {
const all = {};
let matchIndex = 1;
for (const i in handlers) {
if (handlers.hasOwnProperty(i)) {
const h = handlers[i],
groups = h.groups ? h.groups + 1 : 1;
if (!h.literal && h.token) {
all[h.token.val[0]] = h.deser(matches.slice(matchIndex, matchIndex + groups));
}
matchIndex += groups;
}
}
return all;
} else {
return {};
}
}
function dateTimeFromMatches(matches) {
const toField = token => {
switch (token) {
case 'S':
return 'millisecond';
case 's':
return 'second';
case 'm':
return 'minute';
case 'h':
case 'H':
return 'hour';
case 'd':
return 'day';
case 'o':
return 'ordinal';
case 'L':
case 'M':
return 'month';
case 'y':
return 'year';
case 'E':
case 'c':
return 'weekday';
case 'W':
return 'weekNumber';
case 'k':
return 'weekYear';
default:
return null;
}
};
let zone;
if (!Util.isUndefined(matches.Z)) {
zone = new FixedOffsetZone(matches.Z);
} else if (!Util.isUndefined(matches.z)) {
zone = new IANAZone(matches.z);
} else {
zone = null;
}
if (!Util.isUndefined(matches.h) && matches.a === 1) {
matches.h += 12;
}
if (matches.G === 0 && matches.y) {
matches.y = -matches.y;
}
const vals = Object.keys(matches).reduce((r, k) => {
const f = toField(k);
if (f) {
r[f] = matches[k];
}
return r;
}, {});
return [vals, zone];
}
/**
* @private
*/
export class TokenParser {
constructor(loc) {
Object.defineProperty(this, 'loc', { value: loc, enumerable: true });
}
explainParse(input, format) {
const tokens = Formatter.parseFormat(format),
units = tokens.map(t => unitForToken(t, this.loc)),
disqualifyingUnit = units.find(t => t.invalidReason);
if (disqualifyingUnit) {
return { input, tokens, invalidReason: disqualifyingUnit.invalidReason };
} else {
const [regex, handlers] = buildRegex(units),
matches = match(input, RegExp(regex, 'i'), handlers),
[result, zone] = matches ? dateTimeFromMatches(matches) : [null, null];
return { input, tokens, regex, matches, result, zone };
}
}
parseDateTime(input, format) {
const { result, zone, invalidReason } = this.explainParse(input, format);
return [result, zone, invalidReason];
}
}