@formatjs/intl-datetimeformat
Version:
Intl.DateTimeFormat polyfill
246 lines (245 loc) • 6.47 kB
JavaScript
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)
}];
}