From 9c75e4876f68b36cb8a79bfe5a734d2783c527ce Mon Sep 17 00:00:00 2001 From: Tim Reichen Date: Fri, 25 Sep 2020 00:06:22 +0200 Subject: [PATCH] fix(std/datetime):: 12 and 24 support (#7661) --- std/datetime/README.md | 36 ++++++++-------- std/datetime/formatter.ts | 67 +++++++++++++++++++++------- std/datetime/test.ts | 91 +++++++++++++++++++++++++++++---------- std/datetime/tokenizer.ts | 11 +++-- 4 files changed, 147 insertions(+), 58 deletions(-) diff --git a/std/datetime/README.md b/std/datetime/README.md index 62e15df2e3..9678ec4344 100644 --- a/std/datetime/README.md +++ b/std/datetime/README.md @@ -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 diff --git a/std/datetime/formatter.ts b/std/datetime/formatter.ts index 14fb552cd4..6f11090f09 100644 --- a/std/datetime/formatter.ts +++ b/std/datetime/formatter.ts @@ -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); } diff --git a/std/datetime/test.ts b/std/datetime/test.ts index 10de786b6e..9d4e58121f 100644 --- a/std/datetime/test.ts +++ b/std/datetime/test.ts @@ -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); diff --git a/std/datetime/tokenizer.ts b/std/datetime/tokenizer.ts index e56dc52dba..d1b982b1f7 100644 --- a/std/datetime/tokenizer.ts +++ b/std/datetime/tokenizer.ts @@ -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;