Skip to content

Commit

Permalink
fix(datetime): correct parsing of day period (#6313)
Browse files Browse the repository at this point in the history
Co-authored-by: Yoshiya Hinosawa <[email protected]>
  • Loading branch information
timreichen and kt3k authored Jan 10, 2025
1 parent 148107e commit 1ad8eab
Show file tree
Hide file tree
Showing 3 changed files with 248 additions and 12 deletions.
36 changes: 34 additions & 2 deletions datetime/_date_time_formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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.`);
Expand All @@ -491,6 +522,7 @@ export class DateTimeFormatter {
);
}
value = part.value as string;
length = value?.length;
break;
}

Expand All @@ -512,7 +544,7 @@ export class DateTimeFormatter {
}
parts.push({ type, value });

string = string.slice(value.length);
string = string.slice(length);
}

if (string.length) {
Expand Down
216 changes: 206 additions & 10 deletions datetime/_date_time_formatter_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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);
Expand Down
Loading

0 comments on commit 1ad8eab

Please sign in to comment.