Skip to content

Commit 5995f9c

Browse files
authored
fix(datetime): fix formatting of fractionalSecond, fix parsing of yy and yyyy, test each part type in dateTimeFormatter.format (#6516)
1 parent 61f9fd3 commit 5995f9c

File tree

3 files changed

+243
-58
lines changed

3 files changed

+243
-58
lines changed

datetime/_date_time_formatter.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ export class DateTimeFormatter {
295295
const value = utc
296296
? date.getUTCMilliseconds()
297297
: date.getMilliseconds();
298-
string += digits(value, Number(part.value));
298+
string += digits(value, 3).slice(0, Number(part.value));
299299
break;
300300
}
301301
// FIXME(bartlomieju)
@@ -331,12 +331,12 @@ export class DateTimeFormatter {
331331
case "year": {
332332
switch (part.value) {
333333
case "numeric": {
334-
value = /^\d{1,4}/.exec(string)?.[0] as string;
334+
value = /^\d{4}/.exec(string)?.[0] as string;
335335
length = value?.length;
336336
break;
337337
}
338338
case "2-digit": {
339-
value = /^\d{1,2}/.exec(string)?.[0] as string;
339+
value = /^\d{2}/.exec(string)?.[0] as string;
340340
length = value?.length;
341341
break;
342342
}

datetime/_date_time_formatter_test.ts

Lines changed: 226 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,141 @@ import { assertEquals, assertThrows } from "@std/assert";
33
import { FakeTime } from "@std/testing/time";
44
import { DateTimeFormatter } from "./_date_time_formatter.ts";
55

6-
Deno.test("dateTimeFormatter.format()", () => {
7-
const cases = [
8-
["yyyy-MM-dd HH:mm:ss a", new Date(2020, 0, 1), "2020-01-01 00:00:00 AM"],
9-
[
10-
"yyyy-MM-dd HH:mm:ss a",
11-
new Date(2020, 0, 1, 23, 59, 59),
12-
"2020-01-01 23:59:59 PM",
13-
],
14-
[
15-
"yyyy-MM-dd hh:mm:ss a",
16-
new Date(2020, 0, 1, 23, 59, 59),
17-
"2020-01-01 11:59:59 PM",
18-
],
19-
["yyyy-MM-dd a", new Date(2020, 0, 1), "2020-01-01 AM"],
20-
["yyyy-MM-dd HH:mm:ss a", new Date(2020, 0, 1), "2020-01-01 00:00:00 AM"],
21-
["yyyy-MM-dd hh:mm:ss a", new Date(2020, 0, 1), "2020-01-01 12:00:00 AM"],
22-
["yyyy", new Date(2020, 0, 1), "2020"],
23-
["MM", new Date(2020, 0, 1), "01"],
24-
] as const;
25-
for (const [format, date, expected] of cases) {
26-
const formatter = new DateTimeFormatter(format);
27-
assertEquals(formatter.format(date), expected);
28-
}
6+
Deno.test("dateTimeFormatter.format()", async (t) => {
7+
await t.step("handles basic cases", () => {
8+
const cases: [string, Date, string][] = [
9+
["yyyy-MM-dd HH:mm:ss a", new Date(2020, 0, 1), "2020-01-01 00:00:00 AM"],
10+
[
11+
"yyyy-MM-dd HH:mm:ss a",
12+
new Date(2020, 0, 1, 23, 59, 59),
13+
"2020-01-01 23:59:59 PM",
14+
],
15+
[
16+
"yyyy-MM-dd hh:mm:ss a",
17+
new Date(2020, 0, 1, 23, 59, 59),
18+
"2020-01-01 11:59:59 PM",
19+
],
20+
["yyyy-MM-dd a", new Date(2020, 0, 1), "2020-01-01 AM"],
21+
["yyyy-MM-dd HH:mm:ss a", new Date(2020, 0, 1), "2020-01-01 00:00:00 AM"],
22+
["yyyy-MM-dd hh:mm:ss a", new Date(2020, 0, 1), "2020-01-01 12:00:00 AM"],
23+
];
24+
for (const [format, date, expected] of cases) {
25+
const formatter = new DateTimeFormatter(format);
26+
assertEquals(formatter.format(date), expected);
27+
}
28+
});
29+
30+
await t.step("handles yyyy", () => {
31+
const formatter = new DateTimeFormatter("yyyy");
32+
assertEquals(formatter.format(new Date(2020, 0, 1)), "2020");
33+
assertEquals(formatter.format(new Date(20202, 0, 1)), "20202");
34+
});
35+
await t.step("handles yy", () => {
36+
const formatter = new DateTimeFormatter("yy");
37+
assertEquals(formatter.format(new Date(2020, 0, 1)), "20");
38+
assertEquals(formatter.format(new Date(20202, 0, 1)), "02");
39+
});
40+
41+
await t.step("handles MM", () => {
42+
const formatter = new DateTimeFormatter("MM");
43+
assertEquals(formatter.format(new Date(2020, 0, 1)), "01");
44+
assertEquals(formatter.format(new Date(2020, 11, 1)), "12");
45+
});
46+
await t.step("handles M", () => {
47+
const formatter = new DateTimeFormatter("M");
48+
assertEquals(formatter.format(new Date(2020, 0, 1)), "1");
49+
assertEquals(formatter.format(new Date(2020, 11, 1)), "12");
50+
});
51+
52+
await t.step("handles dd", () => {
53+
const formatter = new DateTimeFormatter("dd");
54+
assertEquals(formatter.format(new Date(2020, 0, 1)), "01");
55+
assertEquals(formatter.format(new Date(2020, 0, 22)), "22");
56+
});
57+
await t.step("handles d", () => {
58+
const formatter = new DateTimeFormatter("d");
59+
assertEquals(formatter.format(new Date(2020, 0, 1)), "1");
60+
assertEquals(formatter.format(new Date(2020, 0, 22)), "22");
61+
});
62+
63+
await t.step("handles HH", () => {
64+
const formatter = new DateTimeFormatter("HH");
65+
assertEquals(formatter.format(new Date(2020, 0, 1, 0, 0, 0)), "00");
66+
assertEquals(formatter.format(new Date(2020, 0, 1, 22, 0, 0)), "22");
67+
});
68+
await t.step("handles H", () => {
69+
const formatter = new DateTimeFormatter("H");
70+
assertEquals(formatter.format(new Date(2020, 0, 1, 0, 0, 0)), "0");
71+
assertEquals(formatter.format(new Date(2020, 0, 1, 22, 0, 0)), "22");
72+
});
73+
await t.step("handles hh", () => {
74+
const formatter = new DateTimeFormatter("hh");
75+
assertEquals(formatter.format(new Date(2020, 0, 1, 0, 0, 0)), "12");
76+
assertEquals(formatter.format(new Date(2020, 0, 1, 1, 0, 0)), "01");
77+
assertEquals(formatter.format(new Date(2020, 0, 1, 22, 0, 0)), "10");
78+
});
79+
await t.step("handles h", () => {
80+
const formatter = new DateTimeFormatter("h");
81+
assertEquals(formatter.format(new Date(2020, 0, 1, 0, 0, 0)), "12");
82+
assertEquals(formatter.format(new Date(2020, 0, 1, 1, 0, 0)), "1");
83+
assertEquals(formatter.format(new Date(2020, 0, 1, 22, 0, 0)), "10");
84+
});
85+
86+
await t.step("handles mm", () => {
87+
const formatter = new DateTimeFormatter("mm");
88+
assertEquals(formatter.format(new Date(2020, 0, 1, 0, 0, 0)), "00");
89+
assertEquals(formatter.format(new Date(2020, 0, 1, 0, 1, 0)), "01");
90+
assertEquals(formatter.format(new Date(2020, 0, 1, 0, 22, 0)), "22");
91+
});
92+
await t.step("handles m", () => {
93+
const formatter = new DateTimeFormatter("m");
94+
assertEquals(formatter.format(new Date(2020, 0, 1, 0, 0, 0)), "0");
95+
assertEquals(formatter.format(new Date(2020, 0, 1, 0, 1, 0)), "1");
96+
assertEquals(formatter.format(new Date(2020, 0, 1, 0, 22, 0)), "22");
97+
});
98+
99+
await t.step("handles ss", () => {
100+
const formatter = new DateTimeFormatter("ss");
101+
assertEquals(formatter.format(new Date(2020, 0, 1, 0, 0, 0)), "00");
102+
assertEquals(formatter.format(new Date(2020, 0, 1, 0, 0, 1)), "01");
103+
assertEquals(formatter.format(new Date(2020, 0, 1, 0, 0, 22)), "22");
104+
});
105+
await t.step("handles s", () => {
106+
const formatter = new DateTimeFormatter("s");
107+
assertEquals(formatter.format(new Date(2020, 0, 1, 0, 0, 0)), "0");
108+
assertEquals(formatter.format(new Date(2020, 0, 1, 0, 0, 1)), "1");
109+
assertEquals(formatter.format(new Date(2020, 0, 1, 0, 0, 22)), "22");
110+
});
111+
112+
await t.step("handles SSS", () => {
113+
const formatter = new DateTimeFormatter("SSS");
114+
assertEquals(formatter.format(new Date(2020, 0, 1, 0, 0, 0, 105)), "105");
115+
assertEquals(formatter.format(new Date(2020, 0, 1, 0, 0, 0, 10)), "010");
116+
assertEquals(formatter.format(new Date(2020, 0, 1, 0, 0, 0, 0)), "000");
117+
});
118+
await t.step("handles SS", () => {
119+
const formatter = new DateTimeFormatter("SS");
120+
assertEquals(formatter.format(new Date(2020, 0, 1, 0, 0, 0, 105)), "10");
121+
assertEquals(formatter.format(new Date(2020, 0, 1, 0, 0, 0, 10)), "01");
122+
assertEquals(formatter.format(new Date(2020, 0, 1, 0, 0, 0, 0)), "00");
123+
});
124+
await t.step("handles S", () => {
125+
const formatter = new DateTimeFormatter("S");
126+
assertEquals(formatter.format(new Date(2020, 0, 1, 0, 0, 0, 105)), "1");
127+
assertEquals(formatter.format(new Date(2020, 0, 1, 0, 0, 0, 10)), "0");
128+
assertEquals(formatter.format(new Date(2020, 0, 1, 0, 0, 0, 0)), "0");
129+
});
130+
131+
await t.step("handles utc", () => {
132+
const formatter = new DateTimeFormatter("HH:mm");
133+
assertEquals(
134+
formatter.format(
135+
new Date("2020-01-01T06:30:00.000-01:30"),
136+
{ timeZone: "UTC" },
137+
),
138+
"08:00",
139+
);
140+
});
29141
});
30142

31143
Deno.test("dateTimeFormatter.format() with empty format string returns empty string", () => {
@@ -55,20 +167,66 @@ Deno.test("dateTimeFormatter.formatToParts()", async (t) => {
55167
{ type: "day", value: "01" },
56168
]);
57169
});
170+
await t.step("handles case without separators", () => {
171+
const format = "yyyyMMdd";
172+
const formatter = new DateTimeFormatter(format);
173+
assertEquals(formatter.formatToParts("20200101"), [
174+
{ type: "year", value: "2020" },
175+
{ type: "month", value: "01" },
176+
{ type: "day", value: "01" },
177+
]);
178+
});
179+
180+
await t.step("throws on an empty string", () => {
181+
const format = "yyyy-MM-dd";
182+
const formatter = new DateTimeFormatter(format);
183+
assertThrows(
184+
() => formatter.formatToParts(""),
185+
Error,
186+
"Cannot format value: The value is not valid for part { year undefined } ",
187+
);
188+
});
189+
await t.step("throws on a string which exceeds the format", () => {
190+
const format = "yyyy-MM-dd";
191+
const formatter = new DateTimeFormatter(format);
192+
assertThrows(
193+
() => formatter.formatToParts("2020-01-01T00:00:00.000Z"),
194+
Error,
195+
"datetime string was not fully parsed!",
196+
);
197+
});
198+
await t.step("throws on malformatted year", () => {
199+
const format = "yyyy-MM-dd";
200+
const formatter = new DateTimeFormatter(format);
201+
assertThrows(
202+
() => formatter.formatToParts("20-01-01"),
203+
Error,
204+
"Cannot format value: The value is not valid for part { year undefined } 20",
205+
);
206+
});
58207

59208
await t.step("handles yy", () => {
60209
const format = "yy";
61210
const formatter = new DateTimeFormatter(format);
62211
assertEquals(formatter.formatToParts("20"), [
63212
{ type: "year", value: "20" },
64213
]);
214+
assertEquals(formatter.formatToParts("00"), [
215+
{ type: "year", value: "00" },
216+
]);
217+
assertThrows(() => formatter.formatToParts("2"));
218+
assertThrows(() => formatter.formatToParts("202"));
219+
assertThrows(() => formatter.formatToParts("2020"));
65220
});
66221
await t.step("handles yyyy", () => {
67222
const format = "yyyy";
68223
const formatter = new DateTimeFormatter(format);
69224
assertEquals(formatter.formatToParts("2020"), [
70225
{ type: "year", value: "2020" },
71226
]);
227+
assertThrows(() => formatter.formatToParts("20"));
228+
assertThrows(() => formatter.formatToParts("202"));
229+
assertThrows(() => formatter.formatToParts("20202"));
72230
});
73231
await t.step("handles M", () => {
74232
const format = "M";
@@ -181,64 +339,77 @@ Deno.test("dateTimeFormatter.formatToParts()", async (t) => {
181339
assertEquals(formatter.formatToParts("1"), [
182340
{ type: "fractionalSecond", value: "1" },
183341
]);
342+
assertEquals(formatter.formatToParts("0"), [
343+
{ type: "fractionalSecond", value: "0" },
344+
]);
345+
assertThrows(() => formatter.formatToParts("00"));
184346
});
185347
await t.step("handles SS", () => {
186348
const format = "SS";
187349
const formatter = new DateTimeFormatter(format);
188350
assertEquals(formatter.formatToParts("10"), [
189351
{ type: "fractionalSecond", value: "10" },
190352
]);
353+
assertEquals(formatter.formatToParts("01"), [
354+
{ type: "fractionalSecond", value: "01" },
355+
]);
356+
assertEquals(formatter.formatToParts("00"), [
357+
{ type: "fractionalSecond", value: "00" },
358+
]);
359+
assertThrows(() => formatter.formatToParts("0"));
360+
assertThrows(() => formatter.formatToParts("000"));
191361
});
192362
await t.step("handles SSS", () => {
193363
const format = "SSS";
194364
const formatter = new DateTimeFormatter(format);
195365
assertEquals(formatter.formatToParts("100"), [
196366
{ type: "fractionalSecond", value: "100" },
197367
]);
198-
});
199-
await t.step("handles a", () => {
200-
const format = "a";
201-
const formatter = new DateTimeFormatter(format);
202-
assertEquals(formatter.formatToParts("AM"), [
203-
{ type: "dayPeriod", value: "AM" },
368+
assertEquals(formatter.formatToParts("010"), [
369+
{ type: "fractionalSecond", value: "010" },
370+
]);
371+
assertEquals(formatter.formatToParts("000"), [
372+
{ type: "fractionalSecond", value: "000" },
204373
]);
374+
assertThrows(() => formatter.formatToParts("0"));
375+
assertThrows(() => formatter.formatToParts("0000"));
205376
});
206-
await t.step("handles a AM", () => {
377+
await t.step("handles a: AM", () => {
207378
const format = "a";
208379
const formatter = new DateTimeFormatter(format);
209380
assertEquals(formatter.formatToParts("AM"), [
210381
{ type: "dayPeriod", value: "AM" },
211382
]);
212383
});
213-
await t.step("handles a AM.", () => {
384+
await t.step("handles a: AM.", () => {
214385
const format = "a";
215386
const formatter = new DateTimeFormatter(format);
216387
assertEquals(formatter.formatToParts("AM."), [
217388
{ type: "dayPeriod", value: "AM" },
218389
]);
219390
});
220-
await t.step("handles a A.M.", () => {
391+
await t.step("handles a: A.M.", () => {
221392
const format = "a";
222393
const formatter = new DateTimeFormatter(format);
223394
assertEquals(formatter.formatToParts("A.M."), [
224395
{ type: "dayPeriod", value: "AM" },
225396
]);
226397
});
227-
await t.step("handles a PM", () => {
398+
await t.step("handles a: PM", () => {
228399
const format = "a";
229400
const formatter = new DateTimeFormatter(format);
230401
assertEquals(formatter.formatToParts("PM"), [
231402
{ type: "dayPeriod", value: "PM" },
232403
]);
233404
});
234-
await t.step("handles a PM.", () => {
405+
await t.step("handles a: PM.", () => {
235406
const format = "a";
236407
const formatter = new DateTimeFormatter(format);
237408
assertEquals(formatter.formatToParts("PM."), [
238409
{ type: "dayPeriod", value: "PM" },
239410
]);
240411
});
241-
await t.step("handles a P.M.", () => {
412+
await t.step("handles a: P.M.", () => {
242413
const format = "a";
243414
const formatter = new DateTimeFormatter(format);
244415
assertEquals(formatter.formatToParts("P.M."), [
@@ -247,26 +418,6 @@ Deno.test("dateTimeFormatter.formatToParts()", async (t) => {
247418
});
248419
});
249420

250-
Deno.test("dateTimeFormatter.formatToParts() throws on an empty string", () => {
251-
const format = "yyyy-MM-dd";
252-
const formatter = new DateTimeFormatter(format);
253-
assertThrows(
254-
() => formatter.formatToParts(""),
255-
Error,
256-
"Cannot format value: The value is not valid for part { year undefined } ",
257-
);
258-
});
259-
260-
Deno.test("dateTimeFormatter.formatToParts() throws on a string which exceeds the format", () => {
261-
const format = "yyyy-MM-dd";
262-
const formatter = new DateTimeFormatter(format);
263-
assertThrows(
264-
() => formatter.formatToParts("2020-01-01T00:00:00.000Z"),
265-
Error,
266-
"datetime string was not fully parsed!",
267-
);
268-
});
269-
270421
Deno.test("dateTimeFormatter.partsToDate()", () => {
271422
const date = new Date("2020-01-01T00:00:00.000Z");
272423
using _time = new FakeTime(date);
@@ -286,6 +437,26 @@ Deno.test("dateTimeFormatter.partsToDate()", () => {
286437
]),
287438
date,
288439
);
440+
assertEquals(
441+
formatter.partsToDate([
442+
{ type: "year", value: "20" },
443+
{ type: "month", value: "1" },
444+
{ type: "day", value: "1" },
445+
{ type: "hour", value: "0" },
446+
{ type: "minute", value: "0" },
447+
{ type: "second", value: "0" },
448+
{ type: "fractionalSecond", value: "0" },
449+
{ type: "dayPeriod", value: "AM" },
450+
{ type: "timeZoneName", value: "UTC" },
451+
]),
452+
date,
453+
);
454+
assertEquals(
455+
formatter.partsToDate([
456+
{ type: "timeZoneName", value: "UTC" },
457+
]),
458+
date,
459+
);
289460
});
290461
Deno.test("dateTimeFormatter.partsToDate() works with am dayPeriod", () => {
291462
const date = new Date("2020-01-01T00:00:00.000Z");

0 commit comments

Comments
 (0)