diff --git a/datetime/_date_time_formatter.ts b/datetime/_date_time_formatter.ts index 377b7a3a216e..d7e08b69b5d2 100644 --- a/datetime/_date_time_formatter.ts +++ b/datetime/_date_time_formatter.ts @@ -325,17 +325,19 @@ export class DateTimeFormatter { for (const part of this.#formatParts) { const type = part.type; - + let length = 0; let value = ""; switch (part.type) { case "year": { switch (part.value) { case "numeric": { value = /^\d{1,4}/.exec(string)?.[0] as string; + length = value?.length; break; } case "2-digit": { value = /^\d{1,2}/.exec(string)?.[0] as string; + length = value?.length; break; } default: @@ -349,22 +351,27 @@ export class DateTimeFormatter { switch (part.value) { case "numeric": { value = /^\d{1,2}/.exec(string)?.[0] as string; + length = value?.length; break; } case "2-digit": { value = /^\d{2}/.exec(string)?.[0] as string; + length = value?.length; break; } case "narrow": { value = /^[a-zA-Z]+/.exec(string)?.[0] as string; + length = value?.length; break; } case "short": { value = /^[a-zA-Z]+/.exec(string)?.[0] as string; + length = value?.length; break; } case "long": { value = /^[a-zA-Z]+/.exec(string)?.[0] as string; + length = value?.length; break; } default: @@ -378,10 +385,12 @@ export class DateTimeFormatter { switch (part.value) { case "numeric": { value = /^\d{1,2}/.exec(string)?.[0] as string; + length = value?.length; break; } case "2-digit": { value = /^\d{2}/.exec(string)?.[0] as string; + length = value?.length; break; } default: @@ -395,6 +404,7 @@ export class DateTimeFormatter { switch (part.value) { case "numeric": { value = /^\d{1,2}/.exec(string)?.[0] as string; + length = value?.length; if (part.hour12 && parseInt(value) > 12) { // TODO(iuioiua): Replace with throwing an error // deno-lint-ignore no-console @@ -406,6 +416,7 @@ export class DateTimeFormatter { } case "2-digit": { value = /^\d{2}/.exec(string)?.[0] as string; + length = value?.length; if (part.hour12 && parseInt(value) > 12) { // TODO(iuioiua): Replace with throwing an error // deno-lint-ignore no-console @@ -427,10 +438,12 @@ export class DateTimeFormatter { switch (part.value) { case "numeric": { value = /^\d{1,2}/.exec(string)?.[0] as string; + length = value?.length; break; } case "2-digit": { value = /^\d{2}/.exec(string)?.[0] as string; + length = value?.length; break; } default: @@ -444,10 +457,12 @@ export class DateTimeFormatter { switch (part.value) { case "numeric": { value = /^\d{1,2}/.exec(string)?.[0] as string; + length = value?.length; break; } case "2-digit": { value = /^\d{2}/.exec(string)?.[0] as string; + length = value?.length; break; } default: @@ -460,24 +475,40 @@ export class DateTimeFormatter { case "fractionalSecond": { value = new RegExp(`^\\d{${part.value}}`).exec(string) ?.[0] as string; + length = value?.length; break; } case "timeZoneName": { value = part.value as string; + length = value?.length; break; } case "dayPeriod": { value = /^[AP](?:\.M\.|M\.?)/i.exec(string)?.[0] as string; switch (value.toUpperCase()) { case "AM": + value = "AM"; + length = 2; + break; case "AM.": + value = "AM"; + length = 3; + break; case "A.M.": value = "AM"; + length = 4; break; case "PM": + value = "PM"; + length = 2; + break; case "PM.": + value = "PM"; + length = 3; + break; case "P.M.": value = "PM"; + length = 4; break; default: throw new Error(`DayPeriod '${value}' is not supported.`); @@ -491,6 +522,7 @@ export class DateTimeFormatter { ); } value = part.value as string; + length = value?.length; break; } @@ -512,7 +544,7 @@ export class DateTimeFormatter { } parts.push({ type, value }); - string = string.slice(value.length); + string = string.slice(length); } if (string.length) { diff --git a/datetime/_date_time_formatter_test.ts b/datetime/_date_time_formatter_test.ts index c4d9167b8240..e7c450336d04 100644 --- a/datetime/_date_time_formatter_test.ts +++ b/datetime/_date_time_formatter_test.ts @@ -43,16 +43,208 @@ Deno.test("dateTimeFormatter.parse()", () => { assertEquals(formatter.parse("2020-01-01"), new Date(2020, 0, 1)); }); -Deno.test("dateTimeFormatter.formatToParts()", () => { - const format = "yyyy-MM-dd"; - const formatter = new DateTimeFormatter(format); - assertEquals(formatter.formatToParts("2020-01-01"), [ - { type: "year", value: "2020" }, - { type: "literal", value: "-" }, - { type: "month", value: "01" }, - { type: "literal", value: "-" }, - { type: "day", value: "01" }, - ]); +Deno.test("dateTimeFormatter.formatToParts()", async (t) => { + await t.step("handles basic", () => { + const format = "yyyy-MM-dd"; + const formatter = new DateTimeFormatter(format); + assertEquals(formatter.formatToParts("2020-01-01"), [ + { type: "year", value: "2020" }, + { type: "literal", value: "-" }, + { type: "month", value: "01" }, + { type: "literal", value: "-" }, + { type: "day", value: "01" }, + ]); + }); + + await t.step("handles yy", () => { + const format = "yy"; + const formatter = new DateTimeFormatter(format); + assertEquals(formatter.formatToParts("20"), [ + { type: "year", value: "20" }, + ]); + }); + await t.step("handles yyyy", () => { + const format = "yyyy"; + const formatter = new DateTimeFormatter(format); + assertEquals(formatter.formatToParts("2020"), [ + { type: "year", value: "2020" }, + ]); + }); + await t.step("handles M", () => { + const format = "M"; + const formatter = new DateTimeFormatter(format); + assertEquals(formatter.formatToParts("10"), [ + { type: "month", value: "10" }, + ]); + }); + await t.step("handles MM", () => { + const format = "MM"; + const formatter = new DateTimeFormatter(format); + assertEquals(formatter.formatToParts("10"), [ + { type: "month", value: "10" }, + ]); + }); + await t.step("handles d", () => { + const format = "d"; + const formatter = new DateTimeFormatter(format); + assertEquals(formatter.formatToParts("10"), [ + { type: "day", value: "10" }, + ]); + }); + await t.step("handles dd", () => { + const format = "dd"; + const formatter = new DateTimeFormatter(format); + assertEquals(formatter.formatToParts("10"), [ + { type: "day", value: "10" }, + ]); + }); + await t.step("handles h", () => { + const format = "h"; + const formatter = new DateTimeFormatter(format); + assertEquals(formatter.formatToParts("1"), [ + { type: "hour", value: "1" }, + ]); + }); + await t.step("handles hh", () => { + const format = "hh"; + const formatter = new DateTimeFormatter(format); + assertEquals(formatter.formatToParts("11"), [ + { type: "hour", value: "11" }, + ]); + }); + await t.step("handles h value bigger than 12 warning", () => { + const format = "h"; + const formatter = new DateTimeFormatter(format); + assertEquals(formatter.formatToParts("13"), [ + { type: "hour", value: "13" }, + ]); + }); + await t.step("handles hh value bigger than 12 warning", () => { + const format = "hh"; + const formatter = new DateTimeFormatter(format); + assertEquals(formatter.formatToParts("13"), [ + { type: "hour", value: "13" }, + ]); + }); + await t.step("handles H", () => { + const format = "H"; + const formatter = new DateTimeFormatter(format); + assertEquals(formatter.formatToParts("13"), [ + { type: "hour", value: "13" }, + ]); + }); + await t.step("handles HH", () => { + const format = "HH"; + const formatter = new DateTimeFormatter(format); + assertEquals(formatter.formatToParts("13"), [ + { type: "hour", value: "13" }, + ]); + }); + await t.step("handles m", () => { + const format = "m"; + const formatter = new DateTimeFormatter(format); + assertEquals(formatter.formatToParts("10"), [ + { type: "minute", value: "10" }, + ]); + }); + await t.step("handles mm", () => { + const format = "mm"; + const formatter = new DateTimeFormatter(format); + assertEquals(formatter.formatToParts("10"), [ + { type: "minute", value: "10" }, + ]); + }); + await t.step("handles s", () => { + const format = "s"; + const formatter = new DateTimeFormatter(format); + assertEquals(formatter.formatToParts("10"), [ + { type: "second", value: "10" }, + ]); + }); + await t.step("handles ss", () => { + const format = "ss"; + const formatter = new DateTimeFormatter(format); + assertEquals(formatter.formatToParts("10"), [ + { type: "second", value: "10" }, + ]); + }); + await t.step("handles s", () => { + const format = "s"; + const formatter = new DateTimeFormatter(format); + assertEquals(formatter.formatToParts("10"), [ + { type: "second", value: "10" }, + ]); + }); + await t.step("handles S", () => { + const format = "S"; + const formatter = new DateTimeFormatter(format); + assertEquals(formatter.formatToParts("1"), [ + { type: "fractionalSecond", value: "1" }, + ]); + }); + await t.step("handles SS", () => { + const format = "SS"; + const formatter = new DateTimeFormatter(format); + assertEquals(formatter.formatToParts("10"), [ + { type: "fractionalSecond", value: "10" }, + ]); + }); + await t.step("handles SSS", () => { + const format = "SSS"; + const formatter = new DateTimeFormatter(format); + assertEquals(formatter.formatToParts("100"), [ + { type: "fractionalSecond", value: "100" }, + ]); + }); + await t.step("handles a", () => { + const format = "a"; + const formatter = new DateTimeFormatter(format); + assertEquals(formatter.formatToParts("AM"), [ + { type: "dayPeriod", value: "AM" }, + ]); + }); + await t.step("handles a AM", () => { + const format = "a"; + const formatter = new DateTimeFormatter(format); + assertEquals(formatter.formatToParts("AM"), [ + { type: "dayPeriod", value: "AM" }, + ]); + }); + await t.step("handles a AM.", () => { + const format = "a"; + const formatter = new DateTimeFormatter(format); + assertEquals(formatter.formatToParts("AM."), [ + { type: "dayPeriod", value: "AM" }, + ]); + }); + await t.step("handles a A.M.", () => { + const format = "a"; + const formatter = new DateTimeFormatter(format); + assertEquals(formatter.formatToParts("A.M."), [ + { type: "dayPeriod", value: "AM" }, + ]); + }); + await t.step("handles a PM", () => { + const format = "a"; + const formatter = new DateTimeFormatter(format); + assertEquals(formatter.formatToParts("PM"), [ + { type: "dayPeriod", value: "PM" }, + ]); + }); + await t.step("handles a PM.", () => { + const format = "a"; + const formatter = new DateTimeFormatter(format); + assertEquals(formatter.formatToParts("PM."), [ + { type: "dayPeriod", value: "PM" }, + ]); + }); + await t.step("handles a P.M.", () => { + const format = "a"; + const formatter = new DateTimeFormatter(format); + assertEquals(formatter.formatToParts("P.M."), [ + { type: "dayPeriod", value: "PM" }, + ]); + }); }); Deno.test("dateTimeFormatter.formatToParts() throws on an empty string", () => { @@ -285,6 +477,10 @@ Deno.test("dateTimeFormatter.partsToDate() sets utc", () => { { type: "month", value: "01" }, { type: "timeZoneName", value: "UTC" }, ], date], + ["MM", [ + { type: "month", value: "01" }, + { type: "timeZoneName", value: "UTC" }, + ], date], ] as const; for (const [format, input, output] of cases) { const formatter = new DateTimeFormatter(format); diff --git a/datetime/parse_test.ts b/datetime/parse_test.ts index 5c08a59f67ea..45744c18ad44 100644 --- a/datetime/parse_test.ts +++ b/datetime/parse_test.ts @@ -38,6 +38,14 @@ Deno.test({ parse("01-03-2019 09:33 PM", "MM-dd-yyyy HH:mm a"), new Date(2019, 0, 3, 21, 33), ); + assertEquals( + parse("01-03-2019 09:33 PM.", "MM-dd-yyyy HH:mm a"), + new Date(2019, 0, 3, 21, 33), + ); + assertEquals( + parse("01-03-2019 09:33 P.M.", "MM-dd-yyyy HH:mm a"), + new Date(2019, 0, 3, 21, 33), + ); assertEquals( parse("16:34 03-01-2019", "HH:mm dd-MM-yyyy"), new Date(2019, 0, 3, 16, 34),