UNPKG

luxon

Version:
287 lines (249 loc) 8.72 kB
import { untruncateYear, signedOffset, parseMillis } from './util'; import * as English from './english'; import FixedOffsetZone from '../zones/fixedOffsetZone'; import IANAZone from '../zones/IANAZone'; /* * 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]] = parseInt(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,9}))?)?)?/, 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}|([a-zA-Z_]{1,256}/[a-zA-Z_]{1,256}))?` ), sqlTimeExtensionRegex = RegExp(`(?: ${sqlTimeRegex.source})?`); function extractISOYmd(match, cursor) { const item = { year: parseInt(match[cursor]), month: parseInt(match[cursor + 1]) || 1, day: parseInt(match[cursor + 2]) || 1 }; return [item, null, cursor + 3]; } function extractISOTime(match, cursor) { const item = { hour: parseInt(match[cursor]) || 0, minute: parseInt(match[cursor + 1]) || 0, second: parseInt(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] ? new IANAZone(match[cursor]) : null; return [{}, zone, cursor + 1]; } // ISO duration parsing const isoDuration = /^P(?:(?:(\d{1,9})Y)?(?:(\d{1,9})M)?(?:(\d{1,9})D)?(?:T(?:(\d{1,9})H)?(?:(\d{1,9})M)?(?:(\d{1,9})(?:[.,](\d{1,9}))?S)?)?|(\d{1,9})W)$/; function extractISODuration(match) { const [ , yearStr, monthStr, dayStr, hourStr, minuteStr, secondStr, millisecondsStr, weekStr ] = match; return [ { years: parseInt(yearStr), months: parseInt(monthStr), weeks: parseInt(weekStr), days: parseInt(dayStr), hours: parseInt(hourStr), minutes: parseInt(minuteStr), seconds: parseInt(secondStr), milliseconds: 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(parseInt(yearStr)) : parseInt(yearStr), month: monthStr.length === 2 ? parseInt(monthStr, 10) : English.monthsShort.indexOf(monthStr) + 1, day: parseInt(dayStr), hour: parseInt(hourStr), minute: parseInt(minuteStr) }; if (secondStr) result.second = parseInt(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]; } /** * @private */ export function parseISODate(s) { return parse( s, [ combineRegexes(isoYmdRegex, isoTimeExtensionRegex), combineExtractors(extractISOYmd, extractISOTime, extractISOOffset) ], [ combineRegexes(isoWeekRegex, isoTimeExtensionRegex), combineExtractors(extractISOWeekData, extractISOTime, extractISOOffset) ], [ combineRegexes(isoOrdinalRegex, isoTimeExtensionRegex), combineExtractors(extractISOOrdinalData, extractISOTime) ], [combineRegexes(isoTimeRegex), combineExtractors(extractISOTime, extractISOOffset)] ); } 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]); } export function parseSQL(s) { return parse( s, [ combineRegexes(sqlYmdRegex, sqlTimeExtensionRegex), combineExtractors(extractISOYmd, extractISOTime, extractISOOffset, extractIANAZone) ], [ combineRegexes(sqlTimeRegex), combineExtractors(extractISOTime, extractISOOffset, extractIANAZone) ] ); }