// Loaded from https://unpkg.com/luxon@1.25.0/src/impl/regexParser.js import { untruncateYear, signedOffset, parseInteger, parseMillis, ianaRegex, isUndefined } 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. */ 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 [Object.assign(mergedVals, val), mergedZone || zone, 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))?)/, isoTimeBaseRegex = /(\d\d)(?::?(\d\d)(?::?(\d\d)(?:[.,](\d{1,30}))?)?)?/, isoTimeRegex = RegExp(`${isoTimeBaseRegex.source}${offsetRegex.source}?`), isoTimeExtensionRegex = RegExp(`(?:T${isoTimeRegex.source})?`), isoYmdRegex = /([+-]\d{6}|\d{4})(?:-?(\d\d)(?:-?(\d\d))?)?/, isoWeekRegex = /(\d{4})-?W(\d\d)(?:-?(\d))?/, isoOrdinalRegex = /(\d{4})-?(\d{3})/, extractISOWeekData = simpleParse("weekYear", "weekNumber", "weekDay"), extractISOOrdinalData = simpleParse("year", "ordinal"), sqlYmdRegex = /(\d{4})-(\d\d)-(\d\d)/, // dumbed-down version of the ISO one sqlTimeRegex = RegExp( `${isoTimeBaseRegex.source} ?(?:${offsetRegex.source}|(${ianaRegex.source}))?` ), 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 = { hour: int(match, cursor, 0), minute: int(match, cursor + 1, 0), second: int(match, cursor + 2, 0), millisecond: 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 duration parsing const isoDuration = /^-?P(?:(?:(-?\d{1,9})Y)?(?:(-?\d{1,9})M)?(?:(-?\d{1,9})W)?(?:(-?\d{1,9})D)?(?:T(?:(-?\d{1,9})H)?(?:(-?\d{1,9})M)?(?:(-?\d{1,20})(?:[.,](-?\d{1,9}))?S)?)?)$/; function extractISODuration(match) { const [ s, yearStr, monthStr, weekStr, dayStr, hourStr, minuteStr, secondStr, millisecondsStr ] = match; const hasNegativePrefix = s[0] === "-"; const maybeNegate = num => (num && hasNegativePrefix ? -num : num); return [ { years: maybeNegate(parseInteger(yearStr)), months: maybeNegate(parseInteger(monthStr)), weeks: maybeNegate(parseInteger(weekStr)), days: maybeNegate(parseInteger(dayStr)), hours: maybeNegate(parseInteger(hourStr)), minutes: maybeNegate(parseInteger(minuteStr)), seconds: maybeNegate(parseInteger(secondStr)), milliseconds: maybeNegate(parseMillis(millisecondsStr)) } ]; } // 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|Wedsday|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 ); const extractISOWeekTimeAndOffset = combineExtractors( extractISOWeekData, extractISOTime, extractISOOffset ); const extractISOOrdinalDataAndTime = combineExtractors(extractISOOrdinalData, extractISOTime); const extractISOTimeAndOffset = combineExtractors(extractISOTime, extractISOOffset); /** * @private */ export function parseISODate(s) { return parse( s, [isoYmdWithTimeExtensionRegex, extractISOYmdTimeAndOffset], [isoWeekWithTimeExtensionRegex, extractISOWeekTimeAndOffset], [isoOrdinalWithTimeExtensionRegex, extractISOOrdinalDataAndTime], [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 sqlYmdWithTimeExtensionRegex = combineRegexes(sqlYmdRegex, sqlTimeExtensionRegex); const sqlTimeCombinedRegex = combineRegexes(sqlTimeRegex); const extractISOYmdTimeOffsetAndIANAZone = combineExtractors( extractISOYmd, extractISOTime, extractISOOffset, extractIANAZone ); const extractISOTimeOffsetAndIANAZone = combineExtractors( extractISOTime, extractISOOffset, extractIANAZone ); export function parseSQL(s) { return parse( s, [sqlYmdWithTimeExtensionRegex, extractISOYmdTimeOffsetAndIANAZone], [sqlTimeCombinedRegex, extractISOTimeOffsetAndIANAZone] ); }