mirror of
https://github.com/denoland/std.git
synced 2024-11-21 20:50:22 +00:00
refactor(datetime): cleanup DateTimeFormatter
and parse()
function (#5649)
* initial commit * update * fix * update --------- Co-authored-by: Asher Gomez <ashersaupingomez@gmail.com>
This commit is contained in:
parent
0c64f32cc3
commit
c7a39f0752
@ -36,90 +36,89 @@ type FormatPart = {
|
||||
value: string | number;
|
||||
hour12?: boolean;
|
||||
};
|
||||
type Format = FormatPart[];
|
||||
|
||||
const QUOTED_LITERAL_REGEXP = /^(')(?<value>\\.|[^\']*)\1/;
|
||||
const LITERAL_REGEXP = /^(?<value>.+?\s*)/;
|
||||
const SYMBOL_REGEXP = /^(?<symbol>([a-zA-Z])\2*)/;
|
||||
|
||||
// according to unicode symbols (http://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table)
|
||||
function formatToParts(format: string) {
|
||||
const tokens: Format = [];
|
||||
function formatToFormatParts(format: string) {
|
||||
const formatParts: FormatPart[] = [];
|
||||
let index = 0;
|
||||
while (index < format.length) {
|
||||
const substring = format.slice(index);
|
||||
const symbol = SYMBOL_REGEXP.exec(substring)?.groups?.symbol;
|
||||
switch (symbol) {
|
||||
case "yyyy":
|
||||
tokens.push({ type: "year", value: "numeric" });
|
||||
formatParts.push({ type: "year", value: "numeric" });
|
||||
index += symbol.length;
|
||||
continue;
|
||||
case "yy":
|
||||
tokens.push({ type: "year", value: "2-digit" });
|
||||
formatParts.push({ type: "year", value: "2-digit" });
|
||||
index += symbol.length;
|
||||
continue;
|
||||
case "MM":
|
||||
tokens.push({ type: "month", value: "2-digit" });
|
||||
formatParts.push({ type: "month", value: "2-digit" });
|
||||
index += symbol.length;
|
||||
continue;
|
||||
case "M":
|
||||
tokens.push({ type: "month", value: "numeric" });
|
||||
formatParts.push({ type: "month", value: "numeric" });
|
||||
index += symbol.length;
|
||||
continue;
|
||||
case "dd":
|
||||
tokens.push({ type: "day", value: "2-digit" });
|
||||
formatParts.push({ type: "day", value: "2-digit" });
|
||||
index += symbol.length;
|
||||
continue;
|
||||
case "d":
|
||||
tokens.push({ type: "day", value: "numeric" });
|
||||
formatParts.push({ type: "day", value: "numeric" });
|
||||
index += symbol.length;
|
||||
continue;
|
||||
case "HH":
|
||||
tokens.push({ type: "hour", value: "2-digit" });
|
||||
formatParts.push({ type: "hour", value: "2-digit" });
|
||||
index += symbol.length;
|
||||
continue;
|
||||
case "H":
|
||||
tokens.push({ type: "hour", value: "numeric" });
|
||||
formatParts.push({ type: "hour", value: "numeric" });
|
||||
index += symbol.length;
|
||||
continue;
|
||||
case "hh":
|
||||
tokens.push({ type: "hour", value: "2-digit", hour12: true });
|
||||
formatParts.push({ type: "hour", value: "2-digit", hour12: true });
|
||||
index += symbol.length;
|
||||
continue;
|
||||
case "h":
|
||||
tokens.push({ type: "hour", value: "numeric", hour12: true });
|
||||
formatParts.push({ type: "hour", value: "numeric", hour12: true });
|
||||
index += symbol.length;
|
||||
continue;
|
||||
case "mm":
|
||||
tokens.push({ type: "minute", value: "2-digit" });
|
||||
formatParts.push({ type: "minute", value: "2-digit" });
|
||||
index += symbol.length;
|
||||
continue;
|
||||
case "m":
|
||||
tokens.push({ type: "minute", value: "numeric" });
|
||||
formatParts.push({ type: "minute", value: "numeric" });
|
||||
index += symbol.length;
|
||||
continue;
|
||||
case "ss":
|
||||
tokens.push({ type: "second", value: "2-digit" });
|
||||
formatParts.push({ type: "second", value: "2-digit" });
|
||||
index += symbol.length;
|
||||
continue;
|
||||
case "s":
|
||||
tokens.push({ type: "second", value: "numeric" });
|
||||
formatParts.push({ type: "second", value: "numeric" });
|
||||
index += symbol.length;
|
||||
continue;
|
||||
case "SSS":
|
||||
tokens.push({ type: "fractionalSecond", value: 3 });
|
||||
formatParts.push({ type: "fractionalSecond", value: 3 });
|
||||
index += symbol.length;
|
||||
continue;
|
||||
case "SS":
|
||||
tokens.push({ type: "fractionalSecond", value: 2 });
|
||||
formatParts.push({ type: "fractionalSecond", value: 2 });
|
||||
index += symbol.length;
|
||||
continue;
|
||||
case "S":
|
||||
tokens.push({ type: "fractionalSecond", value: 1 });
|
||||
formatParts.push({ type: "fractionalSecond", value: 1 });
|
||||
index += symbol.length;
|
||||
continue;
|
||||
case "a":
|
||||
tokens.push({ type: "dayPeriod", value: 1 });
|
||||
formatParts.push({ type: "dayPeriod", value: 1 });
|
||||
index += symbol.length;
|
||||
continue;
|
||||
}
|
||||
@ -127,25 +126,48 @@ function formatToParts(format: string) {
|
||||
const quotedLiteralMatch = QUOTED_LITERAL_REGEXP.exec(substring);
|
||||
if (quotedLiteralMatch) {
|
||||
const value = quotedLiteralMatch.groups!.value as string;
|
||||
tokens.push({ type: "literal", value });
|
||||
formatParts.push({ type: "literal", value });
|
||||
index += quotedLiteralMatch[0].length;
|
||||
continue;
|
||||
}
|
||||
|
||||
const literalGroups = LITERAL_REGEXP.exec(substring)!.groups!;
|
||||
const value = literalGroups.value as string;
|
||||
tokens.push({ type: "literal", value });
|
||||
formatParts.push({ type: "literal", value });
|
||||
index += value.length;
|
||||
}
|
||||
|
||||
return tokens;
|
||||
return formatParts;
|
||||
}
|
||||
|
||||
function sortDateTimeFormatParts(
|
||||
parts: DateTimeFormatPart[],
|
||||
): DateTimeFormatPart[] {
|
||||
let result: DateTimeFormatPart[] = [];
|
||||
const typeArray = [
|
||||
"year",
|
||||
"month",
|
||||
"day",
|
||||
"hour",
|
||||
"minute",
|
||||
"second",
|
||||
"fractionalSecond",
|
||||
];
|
||||
for (const type of typeArray) {
|
||||
const current = parts.findIndex((el) => el.type === type);
|
||||
if (current !== -1) {
|
||||
result = result.concat(parts.splice(current, 1));
|
||||
}
|
||||
}
|
||||
result = result.concat(parts);
|
||||
return result;
|
||||
}
|
||||
|
||||
export class DateTimeFormatter {
|
||||
#format: Format;
|
||||
#formatParts: FormatPart[];
|
||||
|
||||
constructor(formatString: string) {
|
||||
this.#format = formatToParts(formatString);
|
||||
this.#formatParts = formatToFormatParts(formatString);
|
||||
}
|
||||
|
||||
format(date: Date, options: Options = {}): string {
|
||||
@ -153,13 +175,13 @@ export class DateTimeFormatter {
|
||||
|
||||
const utc = options.timeZone === "UTC";
|
||||
|
||||
for (const token of this.#format) {
|
||||
const type = token.type;
|
||||
for (const part of this.#formatParts) {
|
||||
const type = part.type;
|
||||
|
||||
switch (type) {
|
||||
case "year": {
|
||||
const value = utc ? date.getUTCFullYear() : date.getFullYear();
|
||||
switch (token.value) {
|
||||
switch (part.value) {
|
||||
case "numeric": {
|
||||
string += value;
|
||||
break;
|
||||
@ -170,14 +192,14 @@ export class DateTimeFormatter {
|
||||
}
|
||||
default:
|
||||
throw Error(
|
||||
`FormatterError: value "${token.value}" is not supported`,
|
||||
`FormatterError: value "${part.value}" is not supported`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "month": {
|
||||
const value = (utc ? date.getUTCMonth() : date.getMonth()) + 1;
|
||||
switch (token.value) {
|
||||
switch (part.value) {
|
||||
case "numeric": {
|
||||
string += value;
|
||||
break;
|
||||
@ -188,14 +210,14 @@ export class DateTimeFormatter {
|
||||
}
|
||||
default:
|
||||
throw Error(
|
||||
`FormatterError: value "${token.value}" is not supported`,
|
||||
`FormatterError: value "${part.value}" is not supported`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "day": {
|
||||
const value = utc ? date.getUTCDate() : date.getDate();
|
||||
switch (token.value) {
|
||||
switch (part.value) {
|
||||
case "numeric": {
|
||||
string += value;
|
||||
break;
|
||||
@ -206,18 +228,18 @@ export class DateTimeFormatter {
|
||||
}
|
||||
default:
|
||||
throw Error(
|
||||
`FormatterError: value "${token.value}" is not supported`,
|
||||
`FormatterError: value "${part.value}" is not supported`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "hour": {
|
||||
let value = utc ? date.getUTCHours() : date.getHours();
|
||||
if (token.hour12) {
|
||||
if (part.hour12) {
|
||||
if (value === 0) value = 12;
|
||||
else if (value > 12) value -= 12;
|
||||
}
|
||||
switch (token.value) {
|
||||
switch (part.value) {
|
||||
case "numeric": {
|
||||
string += value;
|
||||
break;
|
||||
@ -228,14 +250,14 @@ export class DateTimeFormatter {
|
||||
}
|
||||
default:
|
||||
throw Error(
|
||||
`FormatterError: value "${token.value}" is not supported`,
|
||||
`FormatterError: value "${part.value}" is not supported`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "minute": {
|
||||
const value = utc ? date.getUTCMinutes() : date.getMinutes();
|
||||
switch (token.value) {
|
||||
switch (part.value) {
|
||||
case "numeric": {
|
||||
string += value;
|
||||
break;
|
||||
@ -246,14 +268,14 @@ export class DateTimeFormatter {
|
||||
}
|
||||
default:
|
||||
throw Error(
|
||||
`FormatterError: value "${token.value}" is not supported`,
|
||||
`FormatterError: value "${part.value}" is not supported`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "second": {
|
||||
const value = utc ? date.getUTCSeconds() : date.getSeconds();
|
||||
switch (token.value) {
|
||||
switch (part.value) {
|
||||
case "numeric": {
|
||||
string += value;
|
||||
break;
|
||||
@ -264,7 +286,7 @@ export class DateTimeFormatter {
|
||||
}
|
||||
default:
|
||||
throw Error(
|
||||
`FormatterError: value "${token.value}" is not supported`,
|
||||
`FormatterError: value "${part.value}" is not supported`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
@ -273,12 +295,12 @@ export class DateTimeFormatter {
|
||||
const value = utc
|
||||
? date.getUTCMilliseconds()
|
||||
: date.getMilliseconds();
|
||||
string += digits(value, Number(token.value));
|
||||
string += digits(value, Number(part.value));
|
||||
break;
|
||||
}
|
||||
// FIXME(bartlomieju)
|
||||
case "timeZoneName": {
|
||||
// string += utc ? "Z" : token.value
|
||||
// string += utc ? "Z" : part.value
|
||||
break;
|
||||
}
|
||||
case "dayPeriod": {
|
||||
@ -286,28 +308,28 @@ export class DateTimeFormatter {
|
||||
break;
|
||||
}
|
||||
case "literal": {
|
||||
string += token.value;
|
||||
string += part.value;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw Error(`FormatterError: { ${token.type} ${token.value} }`);
|
||||
throw Error(`FormatterError: { ${part.type} ${part.value} }`);
|
||||
}
|
||||
}
|
||||
|
||||
return string;
|
||||
}
|
||||
|
||||
parseToParts(string: string): DateTimeFormatPart[] {
|
||||
formatToParts(string: string): DateTimeFormatPart[] {
|
||||
const parts: DateTimeFormatPart[] = [];
|
||||
|
||||
for (const token of this.#format) {
|
||||
const type = token.type;
|
||||
for (const part of this.#formatParts) {
|
||||
const type = part.type;
|
||||
|
||||
let value = "";
|
||||
switch (token.type) {
|
||||
switch (part.type) {
|
||||
case "year": {
|
||||
switch (token.value) {
|
||||
switch (part.value) {
|
||||
case "numeric": {
|
||||
value = /^\d{1,4}/.exec(string)?.[0] as string;
|
||||
break;
|
||||
@ -318,13 +340,13 @@ export class DateTimeFormatter {
|
||||
}
|
||||
default:
|
||||
throw Error(
|
||||
`ParserError: value "${token.value}" is not supported`,
|
||||
`ParserError: value "${part.value}" is not supported`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "month": {
|
||||
switch (token.value) {
|
||||
switch (part.value) {
|
||||
case "numeric": {
|
||||
value = /^\d{1,2}/.exec(string)?.[0] as string;
|
||||
break;
|
||||
@ -347,13 +369,13 @@ export class DateTimeFormatter {
|
||||
}
|
||||
default:
|
||||
throw Error(
|
||||
`ParserError: value "${token.value}" is not supported`,
|
||||
`ParserError: value "${part.value}" is not supported`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "day": {
|
||||
switch (token.value) {
|
||||
switch (part.value) {
|
||||
case "numeric": {
|
||||
value = /^\d{1,2}/.exec(string)?.[0] as string;
|
||||
break;
|
||||
@ -364,16 +386,16 @@ export class DateTimeFormatter {
|
||||
}
|
||||
default:
|
||||
throw Error(
|
||||
`ParserError: value "${token.value}" is not supported`,
|
||||
`ParserError: value "${part.value}" is not supported`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "hour": {
|
||||
switch (token.value) {
|
||||
switch (part.value) {
|
||||
case "numeric": {
|
||||
value = /^\d{1,2}/.exec(string)?.[0] as string;
|
||||
if (token.hour12 && parseInt(value) > 12) {
|
||||
if (part.hour12 && parseInt(value) > 12) {
|
||||
console.error(
|
||||
`Trying to parse hour greater than 12. Use 'H' instead of 'h'.`,
|
||||
);
|
||||
@ -382,7 +404,7 @@ export class DateTimeFormatter {
|
||||
}
|
||||
case "2-digit": {
|
||||
value = /^\d{2}/.exec(string)?.[0] as string;
|
||||
if (token.hour12 && parseInt(value) > 12) {
|
||||
if (part.hour12 && parseInt(value) > 12) {
|
||||
console.error(
|
||||
`Trying to parse hour greater than 12. Use 'HH' instead of 'hh'.`,
|
||||
);
|
||||
@ -391,13 +413,13 @@ export class DateTimeFormatter {
|
||||
}
|
||||
default:
|
||||
throw Error(
|
||||
`ParserError: value "${token.value}" is not supported`,
|
||||
`ParserError: value "${part.value}" is not supported`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "minute": {
|
||||
switch (token.value) {
|
||||
switch (part.value) {
|
||||
case "numeric": {
|
||||
value = /^\d{1,2}/.exec(string)?.[0] as string;
|
||||
break;
|
||||
@ -408,13 +430,13 @@ export class DateTimeFormatter {
|
||||
}
|
||||
default:
|
||||
throw Error(
|
||||
`ParserError: value "${token.value}" is not supported`,
|
||||
`ParserError: value "${part.value}" is not supported`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "second": {
|
||||
switch (token.value) {
|
||||
switch (part.value) {
|
||||
case "numeric": {
|
||||
value = /^\d{1,2}/.exec(string)?.[0] as string;
|
||||
break;
|
||||
@ -425,18 +447,18 @@ export class DateTimeFormatter {
|
||||
}
|
||||
default:
|
||||
throw Error(
|
||||
`ParserError: value "${token.value}" is not supported`,
|
||||
`ParserError: value "${part.value}" is not supported`,
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "fractionalSecond": {
|
||||
value = new RegExp(`^\\d{${token.value}}`).exec(string)
|
||||
value = new RegExp(`^\\d{${part.value}}`).exec(string)
|
||||
?.[0] as string;
|
||||
break;
|
||||
}
|
||||
case "timeZoneName": {
|
||||
value = token.value as string;
|
||||
value = part.value as string;
|
||||
break;
|
||||
}
|
||||
case "dayPeriod": {
|
||||
@ -444,22 +466,22 @@ export class DateTimeFormatter {
|
||||
break;
|
||||
}
|
||||
case "literal": {
|
||||
if (!string.startsWith(token.value as string)) {
|
||||
if (!string.startsWith(part.value as string)) {
|
||||
throw Error(
|
||||
`Literal "${token.value}" not found "${string.slice(0, 25)}"`,
|
||||
`Literal "${part.value}" not found "${string.slice(0, 25)}"`,
|
||||
);
|
||||
}
|
||||
value = token.value as string;
|
||||
value = part.value as string;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw Error(`${token.type} ${token.value}`);
|
||||
throw Error(`${part.type} ${part.value}`);
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
throw Error(
|
||||
`value not valid for token { ${type} ${value} } ${
|
||||
`value not valid for part { ${type} ${value} } ${
|
||||
string.slice(
|
||||
0,
|
||||
25,
|
||||
@ -481,29 +503,9 @@ export class DateTimeFormatter {
|
||||
return parts;
|
||||
}
|
||||
|
||||
/** sort & filter dateTimeFormatPart */
|
||||
sortDateTimeFormatPart(parts: DateTimeFormatPart[]): DateTimeFormatPart[] {
|
||||
let result: DateTimeFormatPart[] = [];
|
||||
const typeArray = [
|
||||
"year",
|
||||
"month",
|
||||
"day",
|
||||
"hour",
|
||||
"minute",
|
||||
"second",
|
||||
"fractionalSecond",
|
||||
];
|
||||
for (const type of typeArray) {
|
||||
const current = parts.findIndex((el) => el.type === type);
|
||||
if (current !== -1) {
|
||||
result = result.concat(parts.splice(current, 1));
|
||||
}
|
||||
}
|
||||
result = result.concat(parts);
|
||||
return result;
|
||||
}
|
||||
|
||||
partsToDate(parts: DateTimeFormatPart[]): Date {
|
||||
parts = sortDateTimeFormatParts(parts);
|
||||
|
||||
const date = new Date();
|
||||
const utc = parts.find(
|
||||
(part) => part.type === "timeZoneName" && part.value === "UTC",
|
||||
@ -565,8 +567,7 @@ export class DateTimeFormatter {
|
||||
}
|
||||
|
||||
parse(string: string): Date {
|
||||
const parts = this.parseToParts(string);
|
||||
const sortParts = this.sortDateTimeFormatPart(parts);
|
||||
return this.partsToDate(sortParts);
|
||||
const parts = this.formatToParts(string);
|
||||
return this.partsToDate(parts);
|
||||
}
|
||||
}
|
||||
|
@ -36,10 +36,10 @@ Deno.test("dateTimeFormatter.parse()", () => {
|
||||
assertEquals(formatter.parse("2020-01-01"), new Date(2020, 0, 1));
|
||||
});
|
||||
|
||||
Deno.test("dateTimeFormatter.parseToParts()", () => {
|
||||
Deno.test("dateTimeFormatter.formatToParts()", () => {
|
||||
const format = "yyyy-MM-dd";
|
||||
const formatter = new DateTimeFormatter(format);
|
||||
assertEquals(formatter.parseToParts("2020-01-01"), [
|
||||
assertEquals(formatter.formatToParts("2020-01-01"), [
|
||||
{ type: "year", value: "2020" },
|
||||
{ type: "literal", value: "-" },
|
||||
{ type: "month", value: "01" },
|
||||
@ -48,21 +48,21 @@ Deno.test("dateTimeFormatter.parseToParts()", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
Deno.test("dateTimeFormatter.parseToParts() throws on an empty string", () => {
|
||||
Deno.test("dateTimeFormatter.formatToParts() throws on an empty string", () => {
|
||||
const format = "yyyy-MM-dd";
|
||||
const formatter = new DateTimeFormatter(format);
|
||||
assertThrows(
|
||||
() => formatter.parseToParts(""),
|
||||
() => formatter.formatToParts(""),
|
||||
Error,
|
||||
"value not valid for token",
|
||||
"value not valid for part",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("dateTimeFormatter.parseToParts() throws on a string which exceeds the format", () => {
|
||||
Deno.test("dateTimeFormatter.formatToParts() throws on a string which exceeds the format", () => {
|
||||
const format = "yyyy-MM-dd";
|
||||
const formatter = new DateTimeFormatter(format);
|
||||
assertThrows(
|
||||
() => formatter.parseToParts("2020-01-01T00:00:00.000Z"),
|
||||
() => formatter.formatToParts("2020-01-01T00:00:00.000Z"),
|
||||
Error,
|
||||
"datetime string was not fully parsed!",
|
||||
);
|
||||
|
@ -46,7 +46,5 @@ import { DateTimeFormatter } from "./_date_time_formatter.ts";
|
||||
*/
|
||||
export function parse(dateString: string, formatString: string): Date {
|
||||
const formatter = new DateTimeFormatter(formatString);
|
||||
const parts = formatter.parseToParts(dateString);
|
||||
const sortParts = formatter.sortDateTimeFormatPart(parts);
|
||||
return formatter.partsToDate(sortParts);
|
||||
return formatter.parse(dateString);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user