summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTim Reichen <timreichen@users.noreply.github.com>2020-08-15 16:37:17 +0200
committerGitHub <noreply@github.com>2020-08-15 10:37:17 -0400
commit684eddcc6bf0c1446c9aba0cdf001c661c19ab24 (patch)
tree134f5f1609cf9f35295f841e27ba26d64c720795
parentb684df784ef0ecbdf8bc3b6015177b6829420f86 (diff)
feat(std/datetime): generalise parser, add formatter (#6619)
-rw-r--r--std/datetime/README.md61
-rw-r--r--std/datetime/formatter.ts534
-rw-r--r--std/datetime/mod.ts90
-rw-r--r--std/datetime/test.ts98
-rw-r--r--std/datetime/tokenizer.ts69
5 files changed, 728 insertions, 124 deletions
diff --git a/std/datetime/README.md b/std/datetime/README.md
index e9142f48a..ac505ad16 100644
--- a/std/datetime/README.md
+++ b/std/datetime/README.md
@@ -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)
...
```
diff --git a/std/datetime/formatter.ts b/std/datetime/formatter.ts
new file mode 100644
index 000000000..0e872cb40
--- /dev/null
+++ b/std/datetime/formatter.ts
@@ -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);
+ }
+}
diff --git a/std/datetime/mod.ts b/std/datetime/mod.ts
index fe6fe6b3c..e47fe9e01 100644
--- a/std/datetime/mod.ts
+++ b/std/datetime/mod.ts
@@ -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);
}
/**
diff --git a/std/datetime/test.ts b/std/datetime/test.ts
index d5dccee73..7e81d3036 100644
--- a/std/datetime/test.ts
+++ b/std/datetime/test.ts
@@ -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 => {
- // 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!",
- );
+ assertThrows((): void => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (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");
diff --git a/std/datetime/tokenizer.ts b/std/datetime/tokenizer.ts
new file mode 100644
index 000000000..05314b770
--- /dev/null
+++ b/std/datetime/tokenizer.ts
@@ -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;
+ }
+}