UNPKG

luxon

Version:
336 lines (286 loc) 10.5 kB
import { untruncateYear, signedOffset, parseInteger, parseMillis, isUndefined, parseFloating, } from "./util.js"; import * as English from "./english.js"; import FixedOffsetZone from "../zones/fixedOffsetZone.js"; import IANAZone from "../zones/IANAZone.js"; /* * This file handles parsing for well-specified formats. Here's how it works: * Two things go into parsing: a regex to match with and an extractor to take apart the groups in the match. * An extractor is just a function that takes a regex match array and returns a { year: ..., month: ... } object * parse() does the work of executing the regex and applying the extractor. It takes multiple regex/extractor pairs to try in sequence. * Extractors can take a "cursor" representing the offset in the match to look at. This makes it easy to combine extractors. * combineExtractors() does the work of combining them, keeping track of the cursor through multiple extractions. * Some extractions are super dumb and simpleParse and fromStrings help DRY them. */ const ianaRegex = /[A-Za-z_+-]{1,256}(?::?\/[A-Za-z0-9_+-]{1,256}(?:\/[A-Za-z0-9_+-]{1,256})?)?/; function combineRegexes(...regexes) { const full = regexes.reduce((f, r) => f + r.source, ""); return RegExp(`^${full}$`); } function combineExtractors(...extractors) { return (m) => extractors .reduce( ([mergedVals, mergedZone, cursor], ex) => { const [val, zone, next] = ex(m, cursor); return [{ ...mergedVals, ...val }, zone || mergedZone, next]; }, [{}, null, 1] ) .slice(0, 2); } function parse(s, ...patterns) { if (s == null) { return [null, null]; } for (const [regex, extractor] of patterns) { const m = regex.exec(s); if (m) { return extractor(m); } } return [null, null]; } function simpleParse(...keys) { return (match, cursor) => { const ret = {}; let i; for (i = 0; i < keys.length; i++) { ret[keys[i]] = parseInteger(match[cursor + i]); } return [ret, null, cursor + i]; }; } // ISO and SQL parsing const offsetRegex = /(?:(Z)|([+-]\d\d)(?::?(\d\d))?)/; const isoExtendedZone = `(?:${offsetRegex.source}?(?:\\[(${ianaRegex.source})\\])?)?`; const isoTimeBaseRegex = /(\d\d)(?::?(\d\d)(?::?(\d\d)(?:[.,](\d{1,30}))?)?)?/; const isoTimeRegex = RegExp(`${isoTimeBaseRegex.source}${isoExtendedZone}`); const isoTimeExtensionRegex = RegExp(`(?:T${isoTimeRegex.source})?`); const isoYmdRegex = /([+-]\d{6}|\d{4})(?:-?(\d\d)(?:-?(\d\d))?)?/; const isoWeekRegex = /(\d{4})-?W(\d\d)(?:-?(\d))?/; const isoOrdinalRegex = /(\d{4})-?(\d{3})/; const extractISOWeekData = simpleParse("weekYear", "weekNumber", "weekDay"); const extractISOOrdinalData = simpleParse("year", "ordinal"); const sqlYmdRegex = /(\d{4})-(\d\d)-(\d\d)/; // dumbed-down version of the ISO one const sqlTimeRegex = RegExp( `${isoTimeBaseRegex.source} ?(?:${offsetRegex.source}|(${ianaRegex.source}))?` ); const sqlTimeExtensionRegex = RegExp(`(?: ${sqlTimeRegex.source})?`); function int(match, pos, fallback) { const m = match[pos]; return isUndefined(m) ? fallback : parseInteger(m); } function extractISOYmd(match, cursor) { const item = { year: int(match, cursor), month: int(match, cursor + 1, 1), day: int(match, cursor + 2, 1), }; return [item, null, cursor + 3]; } function extractISOTime(match, cursor) { const item = { hours: int(match, cursor, 0), minutes: int(match, cursor + 1, 0), seconds: int(match, cursor + 2, 0), milliseconds: parseMillis(match[cursor + 3]), }; return [item, null, cursor + 4]; } function extractISOOffset(match, cursor) { const local = !match[cursor] && !match[cursor + 1], fullOffset = signedOffset(match[cursor + 1], match[cursor + 2]), zone = local ? null : FixedOffsetZone.instance(fullOffset); return [{}, zone, cursor + 3]; } function extractIANAZone(match, cursor) { const zone = match[cursor] ? IANAZone.create(match[cursor]) : null; return [{}, zone, cursor + 1]; } // ISO time parsing const isoTimeOnly = RegExp(`^T?${isoTimeBaseRegex.source}$`); // ISO duration parsing const isoDuration = /^-?P(?:(?:(-?\d{1,20}(?:\.\d{1,20})?)Y)?(?:(-?\d{1,20}(?:\.\d{1,20})?)M)?(?:(-?\d{1,20}(?:\.\d{1,20})?)W)?(?:(-?\d{1,20}(?:\.\d{1,20})?)D)?(?:T(?:(-?\d{1,20}(?:\.\d{1,20})?)H)?(?:(-?\d{1,20}(?:\.\d{1,20})?)M)?(?:(-?\d{1,20})(?:[.,](-?\d{1,20}))?S)?)?)$/; function extractISODuration(match) { const [s, yearStr, monthStr, weekStr, dayStr, hourStr, minuteStr, secondStr, millisecondsStr] = match; const hasNegativePrefix = s[0] === "-"; const negativeSeconds = secondStr && secondStr[0] === "-"; const maybeNegate = (num, force = false) => num !== undefined && (force || (num && hasNegativePrefix)) ? -num : num; return [ { years: maybeNegate(parseFloating(yearStr)), months: maybeNegate(parseFloating(monthStr)), weeks: maybeNegate(parseFloating(weekStr)), days: maybeNegate(parseFloating(dayStr)), hours: maybeNegate(parseFloating(hourStr)), minutes: maybeNegate(parseFloating(minuteStr)), seconds: maybeNegate(parseFloating(secondStr), secondStr === "-0"), milliseconds: maybeNegate(parseMillis(millisecondsStr), negativeSeconds), }, ]; } // These are a little braindead. EDT *should* tell us that we're in, say, America/New_York // and not just that we're in -240 *right now*. But since I don't think these are used that often // I'm just going to ignore that const obsOffsets = { GMT: 0, EDT: -4 * 60, EST: -5 * 60, CDT: -5 * 60, CST: -6 * 60, MDT: -6 * 60, MST: -7 * 60, PDT: -7 * 60, PST: -8 * 60, }; function fromStrings(weekdayStr, yearStr, monthStr, dayStr, hourStr, minuteStr, secondStr) { const result = { year: yearStr.length === 2 ? untruncateYear(parseInteger(yearStr)) : parseInteger(yearStr), month: English.monthsShort.indexOf(monthStr) + 1, day: parseInteger(dayStr), hour: parseInteger(hourStr), minute: parseInteger(minuteStr), }; if (secondStr) result.second = parseInteger(secondStr); if (weekdayStr) { result.weekday = weekdayStr.length > 3 ? English.weekdaysLong.indexOf(weekdayStr) + 1 : English.weekdaysShort.indexOf(weekdayStr) + 1; } return result; } // RFC 2822/5322 const rfc2822 = /^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),\s)?(\d{1,2})\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(\d{2,4})\s(\d\d):(\d\d)(?::(\d\d))?\s(?:(UT|GMT|[ECMP][SD]T)|([Zz])|(?:([+-]\d\d)(\d\d)))$/; function extractRFC2822(match) { const [ , weekdayStr, dayStr, monthStr, yearStr, hourStr, minuteStr, secondStr, obsOffset, milOffset, offHourStr, offMinuteStr, ] = match, result = fromStrings(weekdayStr, yearStr, monthStr, dayStr, hourStr, minuteStr, secondStr); let offset; if (obsOffset) { offset = obsOffsets[obsOffset]; } else if (milOffset) { offset = 0; } else { offset = signedOffset(offHourStr, offMinuteStr); } return [result, new FixedOffsetZone(offset)]; } function preprocessRFC2822(s) { // Remove comments and folding whitespace and replace multiple-spaces with a single space return s .replace(/\([^()]*\)|[\n\t]/g, " ") .replace(/(\s\s+)/g, " ") .trim(); } // http date const rfc1123 = /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (\d\d) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d{4}) (\d\d):(\d\d):(\d\d) GMT$/, rfc850 = /^(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (\d\d)-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-(\d\d) (\d\d):(\d\d):(\d\d) GMT$/, ascii = /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) ( \d|\d\d) (\d\d):(\d\d):(\d\d) (\d{4})$/; function extractRFC1123Or850(match) { const [, weekdayStr, dayStr, monthStr, yearStr, hourStr, minuteStr, secondStr] = match, result = fromStrings(weekdayStr, yearStr, monthStr, dayStr, hourStr, minuteStr, secondStr); return [result, FixedOffsetZone.utcInstance]; } function extractASCII(match) { const [, weekdayStr, monthStr, dayStr, hourStr, minuteStr, secondStr, yearStr] = match, result = fromStrings(weekdayStr, yearStr, monthStr, dayStr, hourStr, minuteStr, secondStr); return [result, FixedOffsetZone.utcInstance]; } const isoYmdWithTimeExtensionRegex = combineRegexes(isoYmdRegex, isoTimeExtensionRegex); const isoWeekWithTimeExtensionRegex = combineRegexes(isoWeekRegex, isoTimeExtensionRegex); const isoOrdinalWithTimeExtensionRegex = combineRegexes(isoOrdinalRegex, isoTimeExtensionRegex); const isoTimeCombinedRegex = combineRegexes(isoTimeRegex); const extractISOYmdTimeAndOffset = combineExtractors( extractISOYmd, extractISOTime, extractISOOffset, extractIANAZone ); const extractISOWeekTimeAndOffset = combineExtractors( extractISOWeekData, extractISOTime, extractISOOffset, extractIANAZone ); const extractISOOrdinalDateAndTime = combineExtractors( extractISOOrdinalData, extractISOTime, extractISOOffset, extractIANAZone ); const extractISOTimeAndOffset = combineExtractors( extractISOTime, extractISOOffset, extractIANAZone ); /* * @private */ export function parseISODate(s) { return parse( s, [isoYmdWithTimeExtensionRegex, extractISOYmdTimeAndOffset], [isoWeekWithTimeExtensionRegex, extractISOWeekTimeAndOffset], [isoOrdinalWithTimeExtensionRegex, extractISOOrdinalDateAndTime], [isoTimeCombinedRegex, extractISOTimeAndOffset] ); } export function parseRFC2822Date(s) { return parse(preprocessRFC2822(s), [rfc2822, extractRFC2822]); } export function parseHTTPDate(s) { return parse( s, [rfc1123, extractRFC1123Or850], [rfc850, extractRFC1123Or850], [ascii, extractASCII] ); } export function parseISODuration(s) { return parse(s, [isoDuration, extractISODuration]); } const extractISOTimeOnly = combineExtractors(extractISOTime); export function parseISOTimeOnly(s) { return parse(s, [isoTimeOnly, extractISOTimeOnly]); } const sqlYmdWithTimeExtensionRegex = combineRegexes(sqlYmdRegex, sqlTimeExtensionRegex); const sqlTimeCombinedRegex = combineRegexes(sqlTimeRegex); const extractISOTimeOffsetAndIANAZone = combineExtractors( extractISOTime, extractISOOffset, extractIANAZone ); export function parseSQL(s) { return parse( s, [sqlYmdWithTimeExtensionRegex, extractISOYmdTimeAndOffset], [sqlTimeCombinedRegex, extractISOTimeOffsetAndIANAZone] ); }