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:
parent
b684df784e
commit
684eddcc6b
5 changed files with 728 additions and 124 deletions
|
@ -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
534
std/datetime/formatter.ts
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
69
std/datetime/tokenizer.ts
Normal 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;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue