Skip to main content
Module

std/datetime/formatter.ts

Deno standard library
Go to Latest
File
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.// This module is browser compatible.
import { CallbackResult, ReceiverResult, Rule, TestFunction, TestResult, Tokenizer,} from "./tokenizer.ts";
function digits(value: string | number, count = 2): string { return String(value).padStart(count, "0");}
// as declared as in namespace Intltype DateTimeFormatPartTypes = | "day" | "dayPeriod" // | "era" | "hour" | "literal" | "minute" | "month" | "second" | "timeZoneName" // | "weekday" | "year" | "fractionalSecond";
interface DateTimeFormatPart { type: DateTimeFormatPartTypes; value: string;}
type TimeZone = "UTC";
interface Options { timeZone?: TimeZone;}
function createLiteralTestFunction(value: string): TestFunction { return (string: string): TestResult => { return string.startsWith(value) ? { value, length: value.length } : undefined; };}
function createMatchTestFunction(match: RegExp): TestFunction { return (string: string): TestResult => { const result = match.exec(string); if (result) return { value: result, length: result[0].length }; };}
// according to unicode symbols (http://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table)const defaultRules = [ { test: createLiteralTestFunction("yyyy"), fn: (): CallbackResult => ({ type: "year", value: "numeric" }), }, { test: createLiteralTestFunction("yy"), fn: (): CallbackResult => ({ type: "year", value: "2-digit" }), },
{ test: createLiteralTestFunction("MM"), fn: (): CallbackResult => ({ type: "month", value: "2-digit" }), }, { test: createLiteralTestFunction("M"), fn: (): CallbackResult => ({ type: "month", value: "numeric" }), }, { test: createLiteralTestFunction("dd"), fn: (): CallbackResult => ({ type: "day", value: "2-digit" }), }, { test: createLiteralTestFunction("d"), fn: (): CallbackResult => ({ type: "day", value: "numeric" }), },
{ test: createLiteralTestFunction("HH"), fn: (): CallbackResult => ({ type: "hour", value: "2-digit" }), }, { test: createLiteralTestFunction("H"), fn: (): CallbackResult => ({ type: "hour", value: "numeric" }), }, { test: createLiteralTestFunction("hh"), fn: (): CallbackResult => ({ type: "hour", value: "2-digit", hour12: true, }), }, { test: createLiteralTestFunction("h"), fn: (): CallbackResult => ({ type: "hour", value: "numeric", hour12: true, }), }, { test: createLiteralTestFunction("mm"), fn: (): CallbackResult => ({ type: "minute", value: "2-digit" }), }, { test: createLiteralTestFunction("m"), fn: (): CallbackResult => ({ type: "minute", value: "numeric" }), }, { test: createLiteralTestFunction("ss"), fn: (): CallbackResult => ({ type: "second", value: "2-digit" }), }, { test: createLiteralTestFunction("s"), fn: (): CallbackResult => ({ type: "second", value: "numeric" }), }, { test: createLiteralTestFunction("SSS"), fn: (): CallbackResult => ({ type: "fractionalSecond", value: 3 }), }, { test: createLiteralTestFunction("SS"), fn: (): CallbackResult => ({ type: "fractionalSecond", value: 2 }), }, { test: createLiteralTestFunction("S"), fn: (): CallbackResult => ({ type: "fractionalSecond", value: 1 }), },
{ test: createLiteralTestFunction("a"), fn: (value: unknown): CallbackResult => ({ type: "dayPeriod", value: value as string, }), },
// quoted literal { test: createMatchTestFunction(/^(')(?<value>\\.|[^\']*)\1/), fn: (match: unknown): CallbackResult => ({ type: "literal", value: (match as RegExpExecArray).groups!.value as string, }), }, // literal { test: createMatchTestFunction(/^.+?\s*/), fn: (match: unknown): CallbackResult => ({ type: "literal", value: (match as RegExpExecArray)[0], }), },];
type FormatPart = { type: DateTimeFormatPartTypes; value: string | number; hour12?: boolean;};type Format = FormatPart[];
export class DateTimeFormatter { #format: Format;
constructor(formatString: string, rules: Rule[] = defaultRules) { const tokenizer = new Tokenizer(rules); this.#format = tokenizer.tokenize( formatString, ({ type, value, hour12 }) => { const result = { type, value, } as unknown as ReceiverResult; if (hour12) result.hour12 = hour12 as boolean; return result; }, ) as Format; }
format(date: Date, options: Options = {}): string { let string = "";
const utc = options.timeZone === "UTC";
for (const token of this.#format) { const type = token.type;
switch (type) { case "year": { const value = utc ? date.getUTCFullYear() : date.getFullYear(); switch (token.value) { case "numeric": { string += value; break; } case "2-digit": { string += digits(value, 2).slice(-2); break; } default: throw Error( `FormatterError: value "${token.value}" is not supported`, ); } break; } case "month": { const value = (utc ? date.getUTCMonth() : date.getMonth()) + 1; switch (token.value) { case "numeric": { string += value; break; } case "2-digit": { string += digits(value, 2); break; } default: throw Error( `FormatterError: value "${token.value}" is not supported`, ); } break; } case "day": { const value = utc ? date.getUTCDate() : date.getDate(); switch (token.value) { case "numeric": { string += value; break; } case "2-digit": { string += digits(value, 2); break; } default: throw Error( `FormatterError: value "${token.value}" is not supported`, ); } break; } case "hour": { let value = utc ? date.getUTCHours() : date.getHours(); value -= token.hour12 && date.getHours() > 12 ? 12 : 0; switch (token.value) { case "numeric": { string += value; break; } case "2-digit": { string += digits(value, 2); break; } default: throw Error( `FormatterError: value "${token.value}" is not supported`, ); } break; } case "minute": { const value = utc ? date.getUTCMinutes() : date.getMinutes(); switch (token.value) { case "numeric": { string += value; break; } case "2-digit": { string += digits(value, 2); break; } default: throw Error( `FormatterError: value "${token.value}" is not supported`, ); } break; } case "second": { const value = utc ? date.getUTCSeconds() : date.getSeconds(); switch (token.value) { case "numeric": { string += value; break; } case "2-digit": { string += digits(value, 2); break; } default: throw Error( `FormatterError: value "${token.value}" is not supported`, ); } break; } case "fractionalSecond": { const value = utc ? date.getUTCMilliseconds() : date.getMilliseconds(); string += digits(value, Number(token.value)); break; } // FIXME(bartlomieju) case "timeZoneName": { // string += utc ? "Z" : token.value break; } case "dayPeriod": { string += token.value ? (date.getHours() >= 12 ? "PM" : "AM") : ""; break; } case "literal": { string += token.value; break; }
default: throw Error(`FormatterError: { ${token.type} ${token.value} }`); } }
return string; }
parseToParts(string: string): DateTimeFormatPart[] { const parts: DateTimeFormatPart[] = [];
for (const token of this.#format) { const type = token.type;
let value = ""; switch (token.type) { case "year": { switch (token.value) { case "numeric": { value = /^\d{1,4}/.exec(string)?.[0] as string; break; } case "2-digit": { value = /^\d{1,2}/.exec(string)?.[0] as string; break; } } break; } case "month": { switch (token.value) { case "numeric": { value = /^\d{1,2}/.exec(string)?.[0] as string; break; } case "2-digit": { value = /^\d{2}/.exec(string)?.[0] as string; break; } case "narrow": { value = /^[a-zA-Z]+/.exec(string)?.[0] as string; break; } case "short": { value = /^[a-zA-Z]+/.exec(string)?.[0] as string; break; } case "long": { value = /^[a-zA-Z]+/.exec(string)?.[0] as string; break; } default: throw Error( `ParserError: value "${token.value}" is not supported`, ); } break; } case "day": { switch (token.value) { case "numeric": { value = /^\d{1,2}/.exec(string)?.[0] as string; break; } case "2-digit": { value = /^\d{2}/.exec(string)?.[0] as string; break; } default: throw Error( `ParserError: value "${token.value}" is not supported`, ); } break; } case "hour": { switch (token.value) { case "numeric": { value = /^\d{1,2}/.exec(string)?.[0] as string; if (token.hour12 && parseInt(value) > 12) { console.error( `Trying to parse hour greater than 12. Use 'H' instead of 'h'.`, ); } break; } case "2-digit": { value = /^\d{2}/.exec(string)?.[0] as string; if (token.hour12 && parseInt(value) > 12) { console.error( `Trying to parse hour greater than 12. Use 'HH' instead of 'hh'.`, ); } break; } default: throw Error( `ParserError: value "${token.value}" is not supported`, ); } break; } case "minute": { switch (token.value) { case "numeric": { value = /^\d{1,2}/.exec(string)?.[0] as string; break; } case "2-digit": { value = /^\d{2}/.exec(string)?.[0] as string; break; } default: throw Error( `ParserError: value "${token.value}" is not supported`, ); } break; } case "second": { switch (token.value) { case "numeric": { value = /^\d{1,2}/.exec(string)?.[0] as string; break; } case "2-digit": { value = /^\d{2}/.exec(string)?.[0] as string; break; } default: throw Error( `ParserError: value "${token.value}" is not supported`, ); } break; } case "fractionalSecond": { value = new RegExp(`^\\d{${token.value}}`).exec(string) ?.[0] as string; break; } case "timeZoneName": { value = token.value as string; break; } case "dayPeriod": { value = /^(A|P)M/.exec(string)?.[0] as string; break; } case "literal": { if (!string.startsWith(token.value as string)) { throw Error( `Literal "${token.value}" not found "${string.slice(0, 25)}"`, ); } value = token.value as string; break; }
default: throw Error(`${token.type} ${token.value}`); }
if (!value) { throw Error( `value not valid for token { ${type} ${value} } ${ string.slice( 0, 25, ) }`, ); } parts.push({ type, value });
string = string.slice(value.length); }
if (string.length) { throw Error( `datetime string was not fully parsed! ${string.slice(0, 25)}`, ); }
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 { const date = new Date(); const utc = parts.find( (part) => part.type === "timeZoneName" && part.value === "UTC", );
const dayPart = parts.find((part) => part.type === "day");
utc ? date.setUTCHours(0, 0, 0, 0) : date.setHours(0, 0, 0, 0); for (const part of parts) { switch (part.type) { case "year": { const value = Number(part.value.padStart(4, "20")); utc ? date.setUTCFullYear(value) : date.setFullYear(value); break; } case "month": { const value = Number(part.value) - 1; if (dayPart) { utc ? date.setUTCMonth(value, Number(dayPart.value)) : date.setMonth(value, Number(dayPart.value)); } else { utc ? date.setUTCMonth(value) : date.setMonth(value); } break; } case "day": { const value = Number(part.value); utc ? date.setUTCDate(value) : date.setDate(value); break; } case "hour": { let value = Number(part.value); const dayPeriod = parts.find( (part: DateTimeFormatPart) => part.type === "dayPeriod", ); if (dayPeriod?.value === "PM") value += 12; utc ? date.setUTCHours(value) : date.setHours(value); break; } case "minute": { const value = Number(part.value); utc ? date.setUTCMinutes(value) : date.setMinutes(value); break; } case "second": { const value = Number(part.value); utc ? date.setUTCSeconds(value) : date.setSeconds(value); break; } case "fractionalSecond": { const value = Number(part.value); utc ? date.setUTCMilliseconds(value) : date.setMilliseconds(value); break; } } } return date; }
parse(string: string): Date { const parts = this.parseToParts(string); const sortParts = this.sortDateTimeFormatPart(parts); return this.partsToDate(sortParts); }}