1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-23 07:44:48 -05:00

feat(std/datetime): generalise parser, add formatter (#6619)

This commit is contained in:
Tim Reichen 2020-08-15 16:37:17 +02:00 committed by GitHub
parent b684df784e
commit 684eddcc6b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 728 additions and 124 deletions

View file

@ -4,22 +4,63 @@ Simple helper to help parse date strings into `Date`, with additional functions.
## Usage
### parseDate / parseDateTime
The following symbols are supported:
- `parseDate()` - Take an input string and a format to parse the date. Supported
formats are exported in `DateFormat`.
- `parseDateTime()` - Take an input string and a format to parse the dateTime.
Supported formats are exported in `DateTimeFormat`.
- `yyyy` - numeric year
- `yy` - 2-digit year
- `M` - numeric month
- `MM` - 2-digit month
- `d` - numeric day
- `dd` - 2-digit day
- `h` - numeric hour
- `hh` - 2-digit hour
- `m` - numeric minute
- `mm` - 2-digit minute
- `s` - numeric second
- `ss` - 2-digit second
- `S` - 1-digit fractionalSecond
- `SS` - 2-digit fractionalSecond
- `SSS` - 3-digit fractionalSecond
- `a` - dayPeriod, either `AM` or `PM`
- `'foo'` - quoted literal
- `./-` - unquoted literal
### parse
Takes an input `string` and a `formatString` to parse to a `date`.
```ts
import { parseDate, parseDateTime } from 'https://deno.land/std/datetime/mod.ts'
import { parse } from 'https://deno.land/std/datetime/mod.ts'
parseDate("20-01-2019", "dd-mm-yyyy") // output : new Date(2019, 0, 20)
parseDate("2019-01-20", "yyyy-mm-dd") // output : new Date(2019, 0, 20)
parse("20-01-2019", "dd-MM-yyyy") // output : new Date(2019, 0, 20)
parse("2019-01-20", "yyyy-MM-dd") // output : new Date(2019, 0, 20)
parse("2019-01-20", "dd.MM.yyyy") // output : new Date(2019, 0, 20)
parse("01-20-2019 16:34", "MM-dd-yyyy hh:mm") // output : new Date(2019, 0, 20, 16, 34)
parse("01-20-2019 04:34 PM", "MM-dd-yyyy hh:mm a") // output : new Date(2019, 0, 20, 16, 34)
parse("16:34 01-20-2019", "hh:mm MM-dd-yyyy") // output : new Date(2019, 0, 20, 16, 34)
parse("01-20-2019 16:34:23.123", "MM-dd-yyyy hh:mm:ss.SSS") // output : new Date(2019, 0, 20, 16, 34, 23, 123)
...
```
### format
Takes an input `date` and a `formatString` to format to a `string`.
```ts
import { format } from 'https://deno.land/std/datetime/mod.ts'
format(new Date(2019, 0, 20), "dd-MM-yyyy") // output : "20-01-2019"
format(new Date(2019, 0, 20), "yyyy-MM-dd") // output : "2019-01-20"
format(new Date(2019, 0, 20), "dd.MM.yyyy") // output : "2019-01-20"
format(new Date(2019, 0, 20, 16, 34), "MM-dd-yyyy hh:mm") // output : "01-20-2019 16:34"
format(new Date(2019, 0, 20, 16, 34), "MM-dd-yyyy hh:mm a") // output : "01-20-2019 04:34 PM"
format(new Date(2019, 0, 20, 16, 34), "hh:mm MM-dd-yyyy") // output : "16:34 01-20-2019"
format(new Date(2019, 0, 20, 16, 34, 23, 123), "MM-dd-yyyy hh:mm:ss.SSS") // output : "01-20-2019 16:34:23.123"
format(new Date(2019, 0, 20), "'today:' yyyy-MM-dd") // output : "today: 2019-01-20"
parseDateTime("01-20-2019 16:34", "mm-dd-yyyy hh:mm") // output : new Date(2019, 0, 20, 16, 34)
parseDateTime("16:34 01-20-2019", "hh:mm mm-dd-yyyy") // output : new Date(2019, 0, 20, 16, 34)
...
```

534
std/datetime/formatter.ts Normal file
View file

@ -0,0 +1,534 @@
import {
CallbackResult,
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 Intl
type 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://userguide.icu-project.org/formatparse/datetime)
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("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 };
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 }) => ({
type,
value,
})) as Format;
}
format(date: Date, options: Options = {}): string {
let string = "";
const utc = options.timeZone === "UTC";
const hour12 = this.#format.find(
(token: FormatPart) => token.type === "dayPeriod",
);
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 -= 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;
}
case "timeZoneName": {
// string += utc ? "Z" : token.value
// break
}
case "dayPeriod": {
string += hour12 ? (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;
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 "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;
}
partsToDate(parts: DateTimeFormatPart[]): Date {
const date = new Date();
const utc = parts.find(
(part) => part.type === "timeZoneName" && part.value === "UTC",
);
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;
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);
return this.partsToDate(parts);
}
}

View file

@ -1,7 +1,6 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
import { assert } from "../_util/assert.ts";
export type DateFormat = "mm-dd-yyyy" | "dd-mm-yyyy" | "yyyy-mm-dd";
import { DateTimeFormatter } from "./formatter.ts";
export const SECOND = 1e3;
export const MINUTE = SECOND * 60;
@ -20,92 +19,27 @@ enum Day {
Sat,
}
function execForce(reg: RegExp, pat: string): RegExpExecArray {
const v = reg.exec(pat);
assert(v != null);
return v;
}
/**
* Parse date from string using format string
* @param dateStr Date string
* @param dateString Date string
* @param format Format string
* @return Parsed date
*/
export function parseDate(dateStr: string, format: DateFormat): Date {
let m, d, y: string;
let datePattern: RegExp;
switch (format) {
case "mm-dd-yyyy":
datePattern = /^(\d{2})-(\d{2})-(\d{4})$/;
[, m, d, y] = execForce(datePattern, dateStr);
break;
case "dd-mm-yyyy":
datePattern = /^(\d{2})-(\d{2})-(\d{4})$/;
[, d, m, y] = execForce(datePattern, dateStr);
break;
case "yyyy-mm-dd":
datePattern = /^(\d{4})-(\d{2})-(\d{2})$/;
[, y, m, d] = execForce(datePattern, dateStr);
break;
default:
throw new Error("Invalid date format!");
}
return new Date(Number(y), Number(m) - 1, Number(d));
export function parse(dateString: string, formatString: string): Date {
const formatter = new DateTimeFormatter(formatString);
const parts = formatter.parseToParts(dateString);
return formatter.partsToDate(parts);
}
export type DateTimeFormat =
| "mm-dd-yyyy hh:mm"
| "dd-mm-yyyy hh:mm"
| "yyyy-mm-dd hh:mm"
| "hh:mm mm-dd-yyyy"
| "hh:mm dd-mm-yyyy"
| "hh:mm yyyy-mm-dd";
/**
* Parse date & time from string using format string
* @param dateStr Date & time string
* Format date using format string
* @param date Date
* @param format Format string
* @return Parsed date
* @return formatted date string
*/
export function parseDateTime(
datetimeStr: string,
format: DateTimeFormat,
): Date {
let m, d, y, ho, mi: string;
let datePattern: RegExp;
switch (format) {
case "mm-dd-yyyy hh:mm":
datePattern = /^(\d{2})-(\d{2})-(\d{4}) (\d{2}):(\d{2})$/;
[, m, d, y, ho, mi] = execForce(datePattern, datetimeStr);
break;
case "dd-mm-yyyy hh:mm":
datePattern = /^(\d{2})-(\d{2})-(\d{4}) (\d{2}):(\d{2})$/;
[, d, m, y, ho, mi] = execForce(datePattern, datetimeStr);
break;
case "yyyy-mm-dd hh:mm":
datePattern = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})$/;
[, y, m, d, ho, mi] = execForce(datePattern, datetimeStr);
break;
case "hh:mm mm-dd-yyyy":
datePattern = /^(\d{2}):(\d{2}) (\d{2})-(\d{2})-(\d{4})$/;
[, ho, mi, m, d, y] = execForce(datePattern, datetimeStr);
break;
case "hh:mm dd-mm-yyyy":
datePattern = /^(\d{2}):(\d{2}) (\d{2})-(\d{2})-(\d{4})$/;
[, ho, mi, d, m, y] = execForce(datePattern, datetimeStr);
break;
case "hh:mm yyyy-mm-dd":
datePattern = /^(\d{2}):(\d{2}) (\d{4})-(\d{2})-(\d{2})$/;
[, ho, mi, y, m, d] = execForce(datePattern, datetimeStr);
break;
default:
throw new Error("Invalid datetime format!");
}
return new Date(Number(y), Number(m) - 1, Number(d), Number(ho), Number(mi));
export function format(date: Date, formatString: string): string {
const formatter = new DateTimeFormatter(formatString);
return formatter.format(date);
}
/**

View file

@ -3,77 +3,103 @@ import { assert, assertEquals, assertThrows } from "../testing/asserts.ts";
import * as datetime from "./mod.ts";
Deno.test({
name: "[std/datetime] parseDate",
name: "[std/datetime] parse",
fn: () => {
assertEquals(
datetime.parseDateTime("01-03-2019 16:30", "mm-dd-yyyy hh:mm"),
datetime.parse("01-03-2019 16:30", "MM-dd-yyyy hh:mm"),
new Date(2019, 0, 3, 16, 30),
);
assertEquals(
datetime.parseDateTime("03-01-2019 16:31", "dd-mm-yyyy hh:mm"),
datetime.parse("01.03.2019 16:30", "MM.dd.yyyy hh:mm"),
new Date(2019, 0, 3, 16, 30),
);
assertEquals(
datetime.parse("03-01-2019 16:31", "dd-MM-yyyy hh:mm"),
new Date(2019, 0, 3, 16, 31),
);
assertEquals(
datetime.parseDateTime("2019-01-03 16:32", "yyyy-mm-dd hh:mm"),
datetime.parse("2019-01-03 16:32", "yyyy-MM-dd hh:mm"),
new Date(2019, 0, 3, 16, 32),
);
assertEquals(
datetime.parseDateTime("16:33 01-03-2019", "hh:mm mm-dd-yyyy"),
datetime.parse("16:33 01-03-2019", "hh:mm MM-dd-yyyy"),
new Date(2019, 0, 3, 16, 33),
);
assertEquals(
datetime.parseDateTime("16:34 03-01-2019", "hh:mm dd-mm-yyyy"),
datetime.parse("01-03-2019 16:33:23.123", "MM-dd-yyyy hh:mm:ss.SSS"),
new Date(2019, 0, 3, 16, 33, 23, 123),
);
assertEquals(
datetime.parse("01-03-2019 09:33 PM", "MM-dd-yyyy hh:mm a"),
new Date(2019, 0, 3, 21, 33),
);
assertEquals(
datetime.parse("16:34 03-01-2019", "hh:mm dd-MM-yyyy"),
new Date(2019, 0, 3, 16, 34),
);
assertEquals(
datetime.parseDateTime("16:35 2019-01-03", "hh:mm yyyy-mm-dd"),
datetime.parse("16:35 2019-01-03", "hh:mm yyyy-MM-dd"),
new Date(2019, 0, 3, 16, 35),
);
assertEquals(
datetime.parse("01-03-2019", "MM-dd-yyyy"),
new Date(2019, 0, 3),
);
assertEquals(
datetime.parse("03-01-2019", "dd-MM-yyyy"),
new Date(2019, 0, 3),
);
assertEquals(
datetime.parse("2019-01-03", "yyyy-MM-dd"),
new Date(2019, 0, 3),
);
},
});
Deno.test({
name: "[std/datetime] invalidParseDateTimeFormatThrows",
fn: () => {
assertThrows(
(): void => {
assertThrows((): void => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(datetime as any).parseDateTime("2019-01-01 00:00", "x-y-z");
},
Error,
"Invalid datetime format!",
);
(datetime as any).parse("2019-01-01 00:00", "x-y-z");
}, Error);
assertThrows((): void => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(datetime as any).parse("2019-01-01", "x-y-z");
}, Error);
},
});
Deno.test({
name: "[std/datetime] parseDate",
name: "[std/datetime] format",
fn: () => {
assertEquals(
datetime.parseDate("01-03-2019", "mm-dd-yyyy"),
new Date(2019, 0, 3),
"2019-01-01",
datetime.format(new Date("2019-01-01T03:24:00"), "yyyy-MM-dd"),
);
assertEquals(
datetime.parseDate("03-01-2019", "dd-mm-yyyy"),
new Date(2019, 0, 3),
"01.01.2019",
datetime.format(new Date("2019-01-01T03:24:00"), "dd.MM.yyyy"),
);
assertEquals(
datetime.parseDate("2019-01-03", "yyyy-mm-dd"),
new Date(2019, 0, 3),
"03:24:00",
datetime.format(new Date("2019-01-01T03:24:00"), "hh:mm:ss"),
);
},
});
Deno.test({
name: "[std/datetime] invalidParseDateFormatThrows",
fn: () => {
assertThrows(
(): void => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(datetime as any).parseDate("2019-01-01", "x-y-z");
},
Error,
"Invalid date format!",
assertEquals(
"03:24:00.532",
datetime.format(new Date("2019-01-01T03:24:00.532"), "hh:mm:ss.SSS"),
);
assertEquals(
"03:24:00 AM",
datetime.format(new Date("2019-01-01T03:24:00"), "hh:mm:ss a"),
);
assertEquals(
"09:24:00 PM",
datetime.format(new Date("2019-01-01T21:24:00"), "hh:mm:ss a"),
);
assertEquals(
datetime.format(new Date(2019, 0, 20), "'today:' yyyy-MM-dd"),
"today: 2019-01-20",
);
},
});
@ -186,7 +212,7 @@ Deno.test({
});
Deno.test({
name: "[std/datetime] Difference",
name: "[std/datetime] difference",
fn(): void {
const denoInit = new Date("2018/5/14");
const denoRelaseV1 = new Date("2020/5/13");

69
std/datetime/tokenizer.ts Normal file
View file

@ -0,0 +1,69 @@
export type Token = {
type: string;
value: string | number;
index: number;
};
interface ReceiverResult {
[name: string]: string | number;
}
export type CallbackResult = { type: string; value: string | number };
type CallbackFunction = (value: unknown) => CallbackResult;
export type TestResult = { value: unknown; length: number } | undefined;
export type TestFunction = (
string: string,
) => TestResult | undefined;
export interface Rule {
test: TestFunction;
fn: CallbackFunction;
}
export class Tokenizer {
rules: Rule[];
constructor(rules: Rule[] = []) {
this.rules = rules;
}
addRule(test: TestFunction, fn: CallbackFunction): Tokenizer {
this.rules.push({ test, fn });
return this;
}
tokenize(
string: string,
receiver = (token: Token): ReceiverResult => token,
): ReceiverResult[] {
function* generator(rules: Rule[]): IterableIterator<ReceiverResult> {
let index = 0;
for (const rule of rules) {
const result = rule.test(string);
if (result) {
const { value, length } = result;
index += length;
string = string.slice(length);
const token = { ...rule.fn(value), index };
yield receiver(token);
yield* generator(rules);
}
}
}
const tokenGenerator = generator(this.rules);
const tokens: ReceiverResult[] = [];
for (const token of tokenGenerator) {
tokens.push(token);
}
if (string.length) {
throw new Error(
`parser error: string not fully parsed! ${string.slice(0, 25)}`,
);
}
return tokens;
}
}