UNPKG

@formatjs/intl-datetimeformat

Version:
246 lines (245 loc) 6.47 kB
import { RangePatternType } from "@formatjs/ecma402-abstract"; /** * https://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table * Credit: https://github.com/caridy/intl-datetimeformat-pattern/blob/master/index.js * with some tweaks */ const DATE_TIME_REGEX = /(?:[Eec]{1,6}|G{1,5}|[Qq]{1,5}|(?:[yYur]+|U{1,5})|[ML]{1,5}|d{1,2}|D{1,3}|F{1}|[abB]{1,5}|[hkHK]{1,2}|w{1,2}|W{1}|m{1,2}|s{1,2}|[zZOvVxX]{1,4})(?=([^']*'[^']*')*[^']*$)/g; // trim patterns after transformations const expPatternTrimmer = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g; function matchSkeletonPattern(match, result) { const len = match.length; switch (match[0]) { case "G": result.era = len === 4 ? "long" : len === 5 ? "narrow" : "short"; return "{era}"; case "y": case "Y": case "u": case "U": case "r": result.year = len === 2 ? "2-digit" : "numeric"; return "{year}"; case "q": case "Q": throw new RangeError("`w/Q` (quarter) patterns are not supported"); case "M": case "L": result.month = [ "numeric", "2-digit", "short", "long", "narrow" ][len - 1]; return "{month}"; case "w": case "W": throw new RangeError("`w/W` (week of year) patterns are not supported"); case "d": result.day = ["numeric", "2-digit"][len - 1]; return "{day}"; case "D": case "F": case "g": result.day = "numeric"; return "{day}"; case "E": result.weekday = len === 4 ? "long" : len === 5 ? "narrow" : "short"; return "{weekday}"; case "e": result.weekday = [ undefined, undefined, "short", "long", "narrow", "short" ][len - 1]; return "{weekday}"; case "c": result.weekday = [ undefined, undefined, "short", "long", "narrow", "short" ][len - 1]; return "{weekday}"; case "a": case "b": case "B": result.hour12 = true; return "{ampm}"; case "h": result.hour = ["numeric", "2-digit"][len - 1]; result.hour12 = true; return "{hour}"; case "H": result.hour = ["numeric", "2-digit"][len - 1]; return "{hour}"; case "K": result.hour = ["numeric", "2-digit"][len - 1]; result.hour12 = true; return "{hour}"; case "k": result.hour = ["numeric", "2-digit"][len - 1]; return "{hour}"; case "j": case "J": case "C": throw new RangeError("`j/J/C` (hour) patterns are not supported, use `h/H/K/k` instead"); case "m": result.minute = ["numeric", "2-digit"][len - 1]; return "{minute}"; case "s": result.second = ["numeric", "2-digit"][len - 1]; return "{second}"; case "S": case "A": result.second = "numeric"; return "{second}"; case "z": case "Z": case "O": case "v": case "V": case "X": case "x": result.timeZoneName = len < 4 ? "short" : "long"; return "{timeZoneName}"; } return ""; } function skeletonTokenToTable2(c) { switch (c) { case "G": return "era"; case "y": case "Y": case "u": case "U": case "r": return "year"; case "M": case "L": return "month"; case "d": case "D": case "F": case "g": return "day"; case "a": case "b": case "B": return "ampm"; case "h": case "H": case "K": case "k": return "hour"; case "m": return "minute"; case "s": case "S": case "A": return "second"; default: throw new RangeError("Invalid range pattern token"); } } export function processDateTimePattern(pattern, result) { const literals = []; // Use skeleton to populate result, but use mapped pattern to populate pattern let pattern12 = pattern.replace(/'{2}/g, "{apostrophe}").replace(/'(.*?)'/g, (_, literal) => { literals.push(literal); return `$$${literals.length - 1}$$`; }).replace(DATE_TIME_REGEX, (m) => matchSkeletonPattern(m, result || {})); //Restore literals if (literals.length) { pattern12 = pattern12.replace(/\$\$(\d+)\$\$/g, (_, i) => { return literals[+i]; }).replace(/\{apostrophe\}/g, "'"); } // Handle apostrophe-escaped things return [pattern12.replace(/([\s\uFEFF\xA0])\{ampm\}([\s\uFEFF\xA0])/, "$1").replace("{ampm}", "").replace(expPatternTrimmer, ""), pattern12]; } /** * Parse Date time skeleton into Intl.DateTimeFormatOptions * Ref: https://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table * @public * @param skeleton skeleton string */ export function parseDateTimeSkeleton(skeleton, rawPattern = skeleton, rangePatterns, intervalFormatFallback) { const result = { pattern: "", pattern12: "", skeleton, rawPattern, rangePatterns: {}, rangePatterns12: {} }; if (rangePatterns) { for (const k in rangePatterns) { const key = skeletonTokenToTable2(k); const rawPattern = rangePatterns[k]; const intervalResult = { patternParts: [] }; const [pattern, pattern12] = processDateTimePattern(rawPattern, intervalResult); result.rangePatterns[key] = { ...intervalResult, patternParts: splitRangePattern(pattern) }; result.rangePatterns12[key] = { ...intervalResult, patternParts: splitRangePattern(pattern12) }; } } if (intervalFormatFallback) { const patternParts = splitFallbackRangePattern(intervalFormatFallback); result.rangePatterns.default = { patternParts }; result.rangePatterns12.default = { patternParts }; } // Process skeleton skeleton.replace(DATE_TIME_REGEX, (m) => matchSkeletonPattern(m, result)); const [pattern, pattern12] = processDateTimePattern(rawPattern); result.pattern = pattern; result.pattern12 = pattern12; return result; } export function splitFallbackRangePattern(pattern) { const parts = pattern.split(/(\{[0|1]\})/g).filter(Boolean); return parts.map((pattern) => { switch (pattern) { case "{0}": return { source: RangePatternType.startRange, pattern }; case "{1}": return { source: RangePatternType.endRange, pattern }; default: return { source: RangePatternType.shared, pattern }; } }); } export function splitRangePattern(pattern) { const PART_REGEX = /\{(.*?)\}/g; // Map of part and index within the string const parts = {}; let match; let splitIndex = 0; while (match = PART_REGEX.exec(pattern)) { if (!(match[0] in parts)) { parts[match[0]] = match.index; } else { splitIndex = match.index; break; } } if (!splitIndex) { return [{ source: RangePatternType.startRange, pattern }]; } return [{ source: RangePatternType.startRange, pattern: pattern.slice(0, splitIndex) }, { source: RangePatternType.endRange, pattern: pattern.slice(splitIndex) }]; }