fix(std/datetime):: 12 and 24 support (#7661)

This commit is contained in:
Tim Reichen 2020-09-25 00:06:22 +02:00 committed by GitHub
parent 82db91372f
commit 9c75e4876f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 147 additions and 58 deletions

View file

@ -4,7 +4,9 @@ Simple helper to help parse date strings into `Date`, with additional functions.
## Usage
The following symbols are supported:
The following symbols from
[unicode LDML](http://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table)
are supported:
- `yyyy` - numeric year
- `yy` - 2-digit year
@ -13,8 +15,10 @@ The following symbols are supported:
- `d` - numeric day
- `dd` - 2-digit day
- `h` - numeric hour
- `hh` - 2-digit hour
- `H` - numeric hour (0-23 hours)
- `HH` - 2-digit hour (00-23 hours)
- `h` - numeric hour (1-12 hours)
- `hh` - 2-digit hour (01-12 hours)
- `m` - numeric minute
- `mm` - 2-digit minute
- `s` - numeric second
@ -38,10 +42,10 @@ import { parse } from 'https://deno.land/std/datetime/mod.ts'
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 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)
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)
...
```
@ -50,18 +54,16 @@ parse("01-20-2019 16:34:23.123", "MM-dd-yyyy hh:mm:ss.SSS") // output : new Date
Takes an input `date` and a `formatString` to format to a `string`.
```ts
import { format } from 'https://deno.land/std/datetime/mod.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"
...
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"
```
### dayOfYear

View file

@ -5,6 +5,7 @@ import {
TestFunction,
TestResult,
Tokenizer,
ReceiverResult,
} from "./tokenizer.ts";
function digits(value: string | number, count = 2): string {
@ -52,7 +53,7 @@ function createMatchTestFunction(match: RegExp): TestFunction {
};
}
// according to unicode symbols (http://userguide.icu-project.org/formatparse/datetime)
// according to unicode symbols (http://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table)
const defaultRules = [
{
test: createLiteralTestFunction("yyyy"),
@ -81,13 +82,29 @@ const defaultRules = [
},
{
test: createLiteralTestFunction("hh"),
test: createLiteralTestFunction("HH"),
fn: (): CallbackResult => ({ type: "hour", value: "2-digit" }),
},
{
test: createLiteralTestFunction("h"),
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" }),
@ -143,7 +160,11 @@ const defaultRules = [
},
];
type FormatPart = { type: DateTimeFormatPartTypes; value: string | number };
type FormatPart = {
type: DateTimeFormatPartTypes;
value: string | number;
hour12?: boolean;
};
type Format = FormatPart[];
export class DateTimeFormatter {
@ -151,19 +172,23 @@ export class DateTimeFormatter {
constructor(formatString: string, rules: Rule[] = defaultRules) {
const tokenizer = new Tokenizer(rules);
this.#format = tokenizer.tokenize(formatString, ({ type, value }) => ({
type,
value,
})) as Format;
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";
const hour12 = this.#format.find(
(token: FormatPart) => token.type === "dayPeriod",
);
for (const token of this.#format) {
const type = token.type;
@ -225,7 +250,7 @@ export class DateTimeFormatter {
}
case "hour": {
let value = utc ? date.getUTCHours() : date.getHours();
value -= hour12 && date.getHours() > 12 ? 12 : 0;
value -= token.hour12 && date.getHours() > 12 ? 12 : 0;
switch (token.value) {
case "numeric": {
string += value;
@ -290,7 +315,7 @@ export class DateTimeFormatter {
// break
}
case "dayPeriod": {
string += hour12 ? (date.getHours() >= 12 ? "PM" : "AM") : "";
string += token.value ? (date.getHours() >= 12 ? "PM" : "AM") : "";
break;
}
case "literal": {
@ -377,10 +402,20 @@ export class DateTimeFormatter {
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:
@ -425,9 +460,8 @@ export class DateTimeFormatter {
break;
}
case "fractionalSecond": {
value = new RegExp(`^\\d{${token.value}}`).exec(
string,
)?.[0] as string;
value = new RegExp(`^\\d{${token.value}}`).exec(string)
?.[0] as string;
break;
}
case "timeZoneName": {
@ -463,6 +497,7 @@ export class DateTimeFormatter {
);
}
parts.push({ type, value });
string = string.slice(value.length);
}

View file

@ -6,39 +6,43 @@ Deno.test({
name: "[std/datetime] parse",
fn: () => {
assertEquals(
datetime.parse("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.parse("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.parse("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.parse("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.parse("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.parse("01-03-2019 16:33:23.123", "MM-dd-yyyy hh:mm:ss.SSS"),
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"),
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"),
datetime.parse("16:34 03-01-2019", "HH:mm dd-MM-yyyy"),
new Date(2019, 0, 3, 16, 34),
);
assertEquals(
datetime.parse("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(
@ -73,30 +77,73 @@ Deno.test({
Deno.test({
name: "[std/datetime] format",
fn: () => {
// Date
assertEquals(
"2019-01-01",
datetime.format(new Date("2019-01-01T03:24:00"), "yyyy-MM-dd"),
datetime.format(new Date("2019-01-01"), "yyyy-MM-dd"),
);
assertEquals(
"01.01.2019",
datetime.format(new Date("2019-01-01T03:24:00"), "dd.MM.yyyy"),
datetime.format(new Date("2019-01-01"), "dd.MM.yyyy"),
);
// 00 hours
assertEquals(
"01:00:00",
datetime.format(new Date("2019-01-01T01:00:00"), "HH:mm:ss"),
);
assertEquals(
"03:24:00",
datetime.format(new Date("2019-01-01T03:24:00"), "hh:mm:ss"),
"13:00:00",
datetime.format(new Date("2019-01-01T13:00:00"), "HH:mm:ss"),
);
// 12 hours
assertEquals(
"01:00:00",
datetime.format(new Date("2019-01-01T01:00:00"), "hh:mm:ss"),
);
assertEquals(
"03:24:00.532",
datetime.format(new Date("2019-01-01T03:24:00.532"), "hh:mm:ss.SSS"),
"01:00:00",
datetime.format(new Date("2019-01-01T13:00:00"), "hh:mm:ss"),
);
// milliseconds
assertEquals(
"13:00:00.000",
datetime.format(new Date("2019-01-01T13:00:00"), "HH:mm:ss.SSS"),
);
assertEquals(
"03:24:00 AM",
datetime.format(new Date("2019-01-01T03:24:00"), "hh:mm:ss a"),
"13:00:00.000",
datetime.format(new Date("2019-01-01T13:00:00.000"), "HH:mm:ss.SSS"),
);
assertEquals(
"09:24:00 PM",
datetime.format(new Date("2019-01-01T21:24:00"), "hh:mm:ss a"),
"13:00:00.123",
datetime.format(new Date("2019-01-01T13:00:00.123"), "HH:mm:ss.SSS"),
);
// day period
assertEquals(
"01:00:00 AM",
datetime.format(new Date("2019-01-01T01:00:00"), "HH:mm:ss a"),
);
assertEquals(
"01:00:00 AM",
datetime.format(new Date("2019-01-01T01:00:00"), "hh:mm:ss a"),
);
assertEquals(
"01:00:00 PM",
datetime.format(new Date("2019-01-01T13:00:00"), "hh:mm:ss a"),
);
assertEquals(
"21:00:00 PM",
datetime.format(new Date("2019-01-01T21:00:00"), "HH:mm:ss a"),
);
assertEquals(
"09:00:00 PM",
datetime.format(new Date("2019-01-01T21:00:00"), "hh:mm:ss a"),
);
// quoted literal
assertEquals(
datetime.format(new Date(2019, 0, 20), "'today:' yyyy-MM-dd"),
"today: 2019-01-20",
@ -181,9 +228,9 @@ Deno.test({
Deno.test({
name: "[std/datetime] weekOfYear",
fn: () => {
assertEquals(datetime.weekOfYear(new Date("2020-01-05T03:24:00")), 1);
assertEquals(datetime.weekOfYear(new Date("2020-12-28T03:24:00")), 53); // 53 weeks in 2020
assertEquals(datetime.weekOfYear(new Date("2020-06-28T03:24:00")), 26);
assertEquals(datetime.weekOfYear(new Date("2020-01-05T03:00:00")), 1);
assertEquals(datetime.weekOfYear(new Date("2020-12-28T03:00:00")), 53); // 53 weeks in 2020
assertEquals(datetime.weekOfYear(new Date("2020-06-28T03:00:00")), 26);
// iso weeks year starting sunday
assertEquals(datetime.weekOfYear(new Date(2012, 0, 1)), 52);

View file

@ -4,12 +4,17 @@ export type Token = {
type: string;
value: string | number;
index: number;
[key: string]: unknown;
};
interface ReceiverResult {
[name: string]: string | number;
export interface ReceiverResult {
[name: string]: string | number | unknown;
}
export type CallbackResult = { type: string; value: string | number };
export type CallbackResult = {
type: string;
value: string | number;
[key: string]: unknown;
};
type CallbackFunction = (value: unknown) => CallbackResult;
export type TestResult = { value: unknown; length: number } | undefined;