vremel
Version:
JavaScript date utility library for Temporal API
278 lines • 12.1 kB
JavaScript
import { createRecord } from "./_createRecord.js";
import { escapeRegExp } from "./_escapeRegExp.js";
import { tokenize } from "./_ldmrDatePattern.js";
import { unionRegExp } from "./_unionRegExp.js";
const fieldToRegExpMap = createRecord({
y: "(?<year>[1-9]\\d*)",
yyy: "(?<year>\\d{3,})",
yyyy: "(?<year>\\d{4,})",
M: "(?<monthNum>[1-9]\\d?)",
MM: "(?<monthNum>\\d{2})",
L: "(?<monthNum>[1-9]\\d?)",
LL: "(?<monthNum>\\d{2})",
d: "(?<day>[1-9]\\d?)",
dd: "(?<day>\\d{2})",
h: "(?<hour12>[1-9]|1[012])",
hh: "(?<hour12>0[1-9]|1[012])",
H: "(?<hour>1\\d?|2[0-3]?|\\d)",
HH: "(?<hour>[01]\\d|2[0-3])",
K: "(?<hour12>[0-9]|1[01])",
KK: "(?<hour12>0\\d|1[01])",
m: "(?<minute>0|[1-9]\\d?)",
mm: "(?<minute>\\d{2})",
s: "(?<second>0|[1-9]\\d?)",
ss: "(?<second>\\d{2})",
});
function getEraNamesAndLookupTable(style, localeData) {
const eraNamesData = localeData.era?.[style];
if (!eraNamesData) {
throw new Error(`locale data for ${style} style of the eras is not provided`);
}
const table = createRecord();
const monthNames = [];
for (const [monthCode, name] of Object.entries(eraNamesData)) {
monthNames.push(name);
table[name] = monthCode;
}
return [monthNames, table];
}
function getMonthNamesAndLookupTable(form, style, localeData) {
const monthNamesData = localeData.month?.[form]?.[style];
if (!monthNamesData) {
throw new Error(`locale data for ${form} form + ${style} style of the months is not provided`);
}
const table = createRecord();
const monthNames = [];
if (Array.isArray(monthNamesData)) {
monthNamesData.forEach((name, index) => {
monthNames.push(name);
table[name] = index + 1;
});
}
else {
for (const [monthCode, name] of Object.entries(monthNamesData)) {
monthNames.push(name);
table[name] = monthCode;
}
}
return [monthNames, table];
}
function getDayPeriodNamesAndLookupTable(style, localeData) {
const dayPeriodData = localeData.dayPeriod?.[style];
if (!dayPeriodData) {
throw new Error(`locale data for ${style} style of the day periods is not provided`);
}
const table = createRecord();
const dayPeriodNames = [];
for (const [dayPeriodId, name] of Object.entries(dayPeriodData)) {
dayPeriodNames.push(name);
table[name] = dayPeriodId;
}
return [dayPeriodNames, table];
}
function fieldToRegExp(field, localeData, lookupTable) {
if (fieldToRegExpMap[field] !== undefined) {
return fieldToRegExpMap[field];
}
if (field.startsWith("y")) {
return `(?<year>\\d{${field.length},})`;
}
if (field.startsWith("S")) {
return `(?<fracSec>\\d{${field.length}})`;
}
if (field.length <= 5) {
const type = field.length <= 3 ? "abbreviated"
: field.length === 4 ? "wide"
: "narrow";
if (field.startsWith("G")) {
const [eraNames, table] = getEraNamesAndLookupTable(type, localeData);
lookupTable.era = table;
return `(?<era>${unionRegExp(eraNames)})`;
}
if (field.startsWith("M")) {
const [monthNames, table] = getMonthNamesAndLookupTable("format", type, localeData);
lookupTable.month = table;
return `(?<monthName>${unionRegExp(monthNames)})`;
}
if (field.startsWith("L")) {
const [monthNames, table] = getMonthNamesAndLookupTable("standalone", type, localeData);
lookupTable.month = table;
return `(?<monthName>${unionRegExp(monthNames)})`;
}
if (field.startsWith("a")) {
const [eraNames, table] = getDayPeriodNamesAndLookupTable(type, localeData);
lookupTable.dayPeriod = table;
return `(?<dayPeriod>${unionRegExp(eraNames)})`;
}
}
throw new Error(`Unknown field: ${field}`);
}
function compilePattern(pattern, localeData) {
const lookupTable = {
era: {},
month: {},
dayPeriod: {},
};
let regexpString = "";
for (const token of tokenize(pattern)) {
if (token.type === "literal") {
regexpString += escapeRegExp(token.value);
}
else {
regexpString += fieldToRegExp(token.value, localeData, lookupTable);
}
}
return [new RegExp(`^${regexpString}$`), lookupTable];
}
/**
* Return the object parsed from string using the given format string.
*
* ```typescript
* Temporal.PlainDate.from(parse('2/9/2025', 'M/d/yyyy'))
* // 2025-02-09
* ```
*
* Characters between two single quotes (`'`) in the format are escaped.
* Two consecutive single quotes (`''`) in the format always represents one single quote (`'`).
* Letters `A` to `Z` and `a` to `z` are reserved for use as pattern characters, unless they are escaped.
*
* Available field patterns are subset of date field symbols in Unicode CLDR,
* see https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table for details.
*
* Basically 3 characters' field (e.g. `MMM`) means 'abbreviated' style,
* 4 characters' field (e.g. `MMMM`) means 'wide' style,
* 5 characters's field (e.g. `MMMMM`) means 'narrow' style.
*
* Available patterns:
* | unit | field | result examples |
* | ------------------ | ---------------- | --------------------------------------- |
* | era | G, GG, GGG | AD, BC |
* | | GGGG | Anno Domini, Before Christ |
* | | GGGGG | A, B |
* | calendar year | y | 2, 20, 201, 2000, 20020 |
* | | yyy | 002, 020, 201, 2000, 20020 |
* | | yyyy | 0002, 0020, 0201, 2000, 20020 |
* | | yyyyy, yyyyyy... | (at least `n` digits with zero-padding) |
* | month (format) | M | 1, 12 |
* | | MM | 01, 12 |
* | | MMM | Jan, Feb, ... |
* | | MMMM | January, February, ... |
* | | MMMMM | J, F, ... |
* | month (standalone) | L | 1, 12 |
* | | LL | 01, 12 |
* | | LLL | Jan, Feb, ... |
* | | LLLL | January, February, ... |
* | | LLLLL | J, F, ... |
* | day | d | 1, 31 |
* | | dd | 01, 31 |
* | AM, PM | a, aa, aaa | AM, PM |
* | | aaaa | a.m., p.m. |
* | | aaaaa | a, p |
* | hour (1-12) | h | 12, 1, 11 |
* | | hh | 12, 01, 11 |
* | hour (0-23) | H | 0, 1, 23 |
* | | HH | 00, 01, 23 |
* | hour (0-11) | K | 0, 1, 11 |
* | | KK | 00, 01, 11 |
* | minute | m | 0, 59 |
* | | mm | 00, 59 |
* | second | s | 0, 59 |
* | | ss | 00, 59 |
* | fraction of second | S | 1, 8 |
* | | SS | 10, 83 |
* | | SSS, SSSS... | (`n` digits) |
*
* @param dateTimeString string representing a date
* @param pattern date format pattern string
* @param localeData optional locale data (required when using non-numeric pattern fields)
* @returns parse result which can be passed to `Temporal.XX.from` static method.
*/
export function parse(dateTimeString, pattern, localeData = {}) {
const [regexp, table] = compilePattern(pattern, localeData);
const matchResult = dateTimeString.match(regexp);
if (matchResult === null) {
throw new Error(`Doesn't match the pattern \`${pattern}\` for the string \`${dateTimeString}\``);
}
const matchGroups = matchResult.groups;
if (matchGroups === undefined) {
return {};
}
const result = {};
if (matchGroups["era"] !== undefined) {
const eraId = table.era[matchGroups["era"]];
if (eraId === undefined) {
throw new Error("Unknown error");
}
result.era = eraId;
}
if (matchGroups["year"] !== undefined) {
if (matchGroups["era"] !== undefined) {
result.eraYear = parseInt(matchGroups["year"]);
}
else {
result.year = parseInt(matchGroups["year"]);
}
}
if (matchGroups["monthNum"] !== undefined) {
result.month = parseInt(matchGroups["monthNum"]);
}
if (matchGroups["monthName"] !== undefined) {
const month = table.month[matchGroups["monthName"]];
if (month === undefined) {
throw new Error("Unknown error");
}
if (typeof month === "number") {
result.month = month;
}
else {
result.monthCode = month;
}
}
if (matchGroups["day"] !== undefined) {
result.day = parseInt(matchGroups["day"]);
}
if (matchGroups["hour"] !== undefined) {
result.hour = parseInt(matchGroups["hour"]);
}
if (matchGroups["hour12"] !== undefined) {
if (matchGroups["dayPeriod"] === undefined) {
throw new Error("no day period specified");
}
const hour = parseInt(matchGroups["hour12"]);
const dayPeriodId = table.dayPeriod[matchGroups["dayPeriod"]];
if (dayPeriodId === undefined) {
throw new Error("Unknown error");
}
if (dayPeriodId === "am") {
result.hour = hour % 12;
}
else if (dayPeriodId === "pm") {
result.hour = (hour % 12) + 12;
}
else {
throw new Error(`day period '${dayPeriodId}' is not supported`);
}
}
if (matchGroups["minute"] !== undefined) {
const minute = parseInt(matchGroups["minute"]);
if (minute < 0 || minute >= 60) {
throw new Error(`minute value is out of range: ${minute}`);
}
result.minute = minute;
}
if (matchGroups["second"] !== undefined) {
const second = parseInt(matchGroups["second"]);
if (second < 0 || second >= 60) {
throw new Error(`second value is out of range: ${second}`);
}
result.second = second;
}
if (matchGroups["fracSec"] !== undefined) {
const fractionalSecondsWithTrailingZero = matchGroups["fracSec"].padEnd(9, "0");
result.millisecond = parseInt(fractionalSecondsWithTrailingZero.slice(0, 3));
result.microsecond = parseInt(fractionalSecondsWithTrailingZero.slice(3, 6));
result.nanosecond = parseInt(fractionalSecondsWithTrailingZero.slice(6, 9));
}
return result;
}
//# sourceMappingURL=parse.js.map