humanity-deno
Version:
Humanity is a library for humanizing data in a human-readable form.
507 lines (506 loc) • 19.4 kB
JavaScript
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
// This module is browser compatible.
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var _DateTimeFormatter_format;
import { Tokenizer, } from "./tokenizer.js";
function digits(value, count = 2) {
return String(value).padStart(count, "0");
}
function createLiteralTestFunction(value) {
return (string) => {
return string.startsWith(value)
? { value, length: value.length }
: undefined;
};
}
function createMatchTestFunction(match) {
return (string) => {
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: () => ({ type: "year", value: "numeric" }),
},
{
test: createLiteralTestFunction("yy"),
fn: () => ({ type: "year", value: "2-digit" }),
},
{
test: createLiteralTestFunction("MM"),
fn: () => ({ type: "month", value: "2-digit" }),
},
{
test: createLiteralTestFunction("M"),
fn: () => ({ type: "month", value: "numeric" }),
},
{
test: createLiteralTestFunction("dd"),
fn: () => ({ type: "day", value: "2-digit" }),
},
{
test: createLiteralTestFunction("d"),
fn: () => ({ type: "day", value: "numeric" }),
},
{
test: createLiteralTestFunction("HH"),
fn: () => ({ type: "hour", value: "2-digit" }),
},
{
test: createLiteralTestFunction("H"),
fn: () => ({ type: "hour", value: "numeric" }),
},
{
test: createLiteralTestFunction("hh"),
fn: () => ({
type: "hour",
value: "2-digit",
hour12: true,
}),
},
{
test: createLiteralTestFunction("h"),
fn: () => ({
type: "hour",
value: "numeric",
hour12: true,
}),
},
{
test: createLiteralTestFunction("mm"),
fn: () => ({ type: "minute", value: "2-digit" }),
},
{
test: createLiteralTestFunction("m"),
fn: () => ({ type: "minute", value: "numeric" }),
},
{
test: createLiteralTestFunction("ss"),
fn: () => ({ type: "second", value: "2-digit" }),
},
{
test: createLiteralTestFunction("s"),
fn: () => ({ type: "second", value: "numeric" }),
},
{
test: createLiteralTestFunction("SSS"),
fn: () => ({ type: "fractionalSecond", value: 3 }),
},
{
test: createLiteralTestFunction("SS"),
fn: () => ({ type: "fractionalSecond", value: 2 }),
},
{
test: createLiteralTestFunction("S"),
fn: () => ({ type: "fractionalSecond", value: 1 }),
},
{
test: createLiteralTestFunction("a"),
fn: (value) => ({
type: "dayPeriod",
value: value,
}),
},
// quoted literal
{
test: createMatchTestFunction(/^(')(?<value>\\.|[^\']*)\1/),
fn: (match) => ({
type: "literal",
value: match.groups.value,
}),
},
// literal
{
test: createMatchTestFunction(/^.+?\s*/),
fn: (match) => ({
type: "literal",
value: match[0],
}),
},
];
export class DateTimeFormatter {
constructor(formatString, rules = defaultRules) {
_DateTimeFormatter_format.set(this, void 0);
const tokenizer = new Tokenizer(rules);
__classPrivateFieldSet(this, _DateTimeFormatter_format, tokenizer.tokenize(formatString, ({ type, value, hour12 }) => {
const result = {
type,
value,
};
if (hour12)
result.hour12 = hour12;
return result;
}), "f");
}
format(date, options = {}) {
let string = "";
const utc = options.timeZone === "UTC";
for (const token of __classPrivateFieldGet(this, _DateTimeFormatter_format, "f")) {
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) {
const parts = [];
for (const token of __classPrivateFieldGet(this, _DateTimeFormatter_format, "f")) {
const type = token.type;
let value = "";
switch (token.type) {
case "year": {
switch (token.value) {
case "numeric": {
value = /^\d{1,4}/.exec(string)?.[0];
break;
}
case "2-digit": {
value = /^\d{1,2}/.exec(string)?.[0];
break;
}
}
break;
}
case "month": {
switch (token.value) {
case "numeric": {
value = /^\d{1,2}/.exec(string)?.[0];
break;
}
case "2-digit": {
value = /^\d{2}/.exec(string)?.[0];
break;
}
case "narrow": {
value = /^[a-zA-Z]+/.exec(string)?.[0];
break;
}
case "short": {
value = /^[a-zA-Z]+/.exec(string)?.[0];
break;
}
case "long": {
value = /^[a-zA-Z]+/.exec(string)?.[0];
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];
break;
}
case "2-digit": {
value = /^\d{2}/.exec(string)?.[0];
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];
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];
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];
break;
}
case "2-digit": {
value = /^\d{2}/.exec(string)?.[0];
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];
break;
}
case "2-digit": {
value = /^\d{2}/.exec(string)?.[0];
break;
}
default:
throw Error(`ParserError: value "${token.value}" is not supported`);
}
break;
}
case "fractionalSecond": {
value = new RegExp(`^\\d{${token.value}}`).exec(string)?.[0];
break;
}
case "timeZoneName": {
value = token.value;
break;
}
case "dayPeriod": {
value = /^(A|P)M/.exec(string)?.[0];
break;
}
case "literal": {
if (!string.startsWith(token.value)) {
throw Error(`Literal "${token.value}" not found "${string.slice(0, 25)}"`);
}
value = token.value;
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) {
let result = [];
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) {
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) => 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) {
const parts = this.parseToParts(string);
const sortParts = this.sortDateTimeFormatPart(parts);
return this.partsToDate(sortParts);
}
}
_DateTimeFormatter_format = new WeakMap();