LibJS+LibUnicode: Integrate Temporal into Intl.DateTimeFormat

The gist is that we need to construct an ICU date-time formatter for
each possible Temporal type. This is of course going to be expensive.
So instead, we construct the configurations needed for the ICU objects
in the Intl.DateTimeFormat constructor, and defer creating the actual
ICU objects until they are needed.

Each formatting prototype can also now accept either a number (as they
already did), or any of the supported Temporal objects. These types may
not be mixed, and their properties (namely, their calendar) must align
with the Intl.DateTimeFormat object.
This commit is contained in:
Timothy Flynn 2024-11-27 16:09:41 -05:00 committed by Andreas Kling
commit ea503a4f68
Notes: github-actions[bot] 2024-11-29 08:53:47 +00:00
12 changed files with 1348 additions and 210 deletions

View file

@ -32,6 +32,49 @@ describe("errors", () => {
Intl.DateTimeFormat().format(8.65e15);
}).toThrowWithMessage(RangeError, "Time value must be between -8.64E15 and 8.64E15");
});
test("Temporal object must have same calendar", () => {
const formatter = new Intl.DateTimeFormat([], { calendar: "iso8601" });
expect(() => {
formatter.format(new Temporal.PlainDate(1972, 1, 1, "gregory"));
}).toThrowWithMessage(
RangeError,
"Cannot format Temporal.PlainDate with calendar 'gregory' in locale with calendar 'iso8601'"
);
expect(() => {
formatter.format(new Temporal.PlainYearMonth(1972, 1, "gregory"));
}).toThrowWithMessage(
RangeError,
"Cannot format Temporal.PlainYearMonth with calendar 'gregory' in locale with calendar 'iso8601'"
);
expect(() => {
formatter.format(new Temporal.PlainMonthDay(1, 1, "gregory"));
}).toThrowWithMessage(
RangeError,
"Cannot format Temporal.PlainMonthDay with calendar 'gregory' in locale with calendar 'iso8601'"
);
expect(() => {
formatter.format(
new Temporal.PlainDateTime(1972, 1, 1, 8, 45, 56, 123, 345, 789, "gregory")
);
}).toThrowWithMessage(
RangeError,
"Cannot format Temporal.PlainDateTime with calendar 'gregory' in locale with calendar 'iso8601'"
);
});
test("cannot format Temporal.ZonedDateTime", () => {
expect(() => {
new Intl.DateTimeFormat().format(new Temporal.ZonedDateTime(0n, "UTC"));
}).toThrowWithMessage(
TypeError,
"Cannot format Temporal.ZonedDateTime, use Temporal.ZonedDateTime.prototype.toLocaleString"
);
});
});
const d0 = Date.UTC(2021, 11, 7, 17, 40, 50, 456);
@ -575,3 +618,35 @@ describe("non-Gregorian calendars", () => {
expect(zh.format(d1)).toBe("1988戊辰年腊月十六 UTC 07:08:09");
});
});
describe("Temporal objects", () => {
const formatter = new Intl.DateTimeFormat("en", {
calendar: "iso8601",
timeZone: "UTC",
});
test("Temporal.PlainDate", () => {
const plainDate = new Temporal.PlainDate(1989, 1, 23);
expect(formatter.format(plainDate)).toBe("1/23/1989");
});
test("Temporal.PlainYearMonth", () => {
const plainYearMonth = new Temporal.PlainYearMonth(1989, 1);
expect(formatter.format(plainYearMonth)).toBe("1/1989");
});
test("Temporal.PlainMonthDay", () => {
const plainMonthDay = new Temporal.PlainMonthDay(1, 23);
expect(formatter.format(plainMonthDay)).toBe("1/23");
});
test("Temporal.PlainTime", () => {
const plainTime = new Temporal.PlainTime(8, 10, 51);
expect(formatter.format(plainTime)).toBe("8:10:51 AM");
});
test("Temporal.Instant", () => {
const instant = new Temporal.Instant(1732740069000000000n);
expect(formatter.format(instant)).toBe("11/27/2024, 8:41:09 PM");
});
});

View file

@ -36,6 +36,82 @@ describe("errors", () => {
}).toThrowWithMessage(RangeError, "Time value must be between -8.64E15 and 8.64E15");
});
});
test("Temporal object must have same calendar", () => {
const formatter = new Intl.DateTimeFormat([], { calendar: "iso8601" });
expect(() => {
const plainDate = new Temporal.PlainDate(1972, 1, 1, "gregory");
formatter.formatRange(plainDate, plainDate);
}).toThrowWithMessage(
RangeError,
"Cannot format Temporal.PlainDate with calendar 'gregory' in locale with calendar 'iso8601'"
);
expect(() => {
const plainYearMonth = new Temporal.PlainYearMonth(1972, 1, "gregory");
formatter.formatRange(plainYearMonth, plainYearMonth);
}).toThrowWithMessage(
RangeError,
"Cannot format Temporal.PlainYearMonth with calendar 'gregory' in locale with calendar 'iso8601'"
);
expect(() => {
const plainMonthDay = new Temporal.PlainMonthDay(1, 1, "gregory");
formatter.formatRange(plainMonthDay, plainMonthDay);
}).toThrowWithMessage(
RangeError,
"Cannot format Temporal.PlainMonthDay with calendar 'gregory' in locale with calendar 'iso8601'"
);
expect(() => {
const plainDateTime = new Temporal.PlainDateTime(
1972,
1,
1,
8,
45,
56,
123,
345,
789,
"gregory"
);
formatter.formatRange(plainDateTime, plainDateTime);
}).toThrowWithMessage(
RangeError,
"Cannot format Temporal.PlainDateTime with calendar 'gregory' in locale with calendar 'iso8601'"
);
});
test("cannot format Temporal.ZonedDateTime", () => {
expect(() => {
const zonedDateTime = new Temporal.ZonedDateTime(0n, "UTC");
new Intl.DateTimeFormat().formatRange(zonedDateTime, zonedDateTime);
}).toThrowWithMessage(
TypeError,
"Cannot format Temporal.ZonedDateTime, use Temporal.ZonedDateTime.prototype.toLocaleString"
);
});
test("cannot mix Temporal object types", () => {
expect(() => {
const plainDate = new Temporal.PlainDate(1972, 1, 1, "gregory");
new Intl.DateTimeFormat().formatRange(plainDate, 0);
}).toThrowWithMessage(
TypeError,
"Cannot format a date-time range with different date-time types"
);
expect(() => {
const plainYearMonth = new Temporal.PlainYearMonth(1972, 1, "gregory");
const plainMonthDay = new Temporal.PlainMonthDay(1, 1, "gregory");
new Intl.DateTimeFormat().formatRange(plainYearMonth, plainMonthDay);
}).toThrowWithMessage(
TypeError,
"Cannot format a date-time range with different date-time types"
);
});
});
const d0 = Date.UTC(1989, 0, 23, 7, 8, 9, 45);
@ -220,3 +296,42 @@ describe("dateStyle + timeStyle", () => {
});
});
});
describe("Temporal objects", () => {
const formatter = new Intl.DateTimeFormat("en", {
calendar: "iso8601",
timeZone: "UTC",
});
test("Temporal.PlainDate", () => {
const plainDate1 = new Temporal.PlainDate(1989, 1, 23);
const plainDate2 = new Temporal.PlainDate(2024, 11, 27);
expect(formatter.formatRange(plainDate1, plainDate2)).toBe("1/23/1989 11/27/2024");
});
test("Temporal.PlainYearMonth", () => {
const plainYearMonth1 = new Temporal.PlainYearMonth(1989, 1);
const plainYearMonth2 = new Temporal.PlainYearMonth(2024, 11);
expect(formatter.formatRange(plainYearMonth1, plainYearMonth2)).toBe("1/1989 11/2024");
});
test("Temporal.PlainMonthDay", () => {
const plainMonthDay1 = new Temporal.PlainMonthDay(1, 23);
const plainMonthDay2 = new Temporal.PlainMonthDay(11, 27);
expect(formatter.formatRange(plainMonthDay1, plainMonthDay2)).toBe("1/23 11/27");
});
test("Temporal.PlainTime", () => {
const plainTime1 = new Temporal.PlainTime(8, 10, 51);
const plainTime2 = new Temporal.PlainTime(20, 41, 9);
expect(formatter.formatRange(plainTime1, plainTime2)).toBe("8:10:51 AM 8:41:09 PM");
});
test("Temporal.Instant", () => {
const instant1 = new Temporal.Instant(601546251000000000n);
const instant2 = new Temporal.Instant(1732740069000000000n);
expect(formatter.formatRange(instant1, instant2)).toBe(
"1/23/1989, 8:10:51 AM 11/27/2024, 8:41:09 PM"
);
});
});

View file

@ -36,6 +36,82 @@ describe("errors", () => {
}).toThrowWithMessage(RangeError, "Time value must be between -8.64E15 and 8.64E15");
});
});
test("Temporal object must have same calendar", () => {
const formatter = new Intl.DateTimeFormat([], { calendar: "iso8601" });
expect(() => {
const plainDate = new Temporal.PlainDate(1972, 1, 1, "gregory");
formatter.formatRangeToParts(plainDate, plainDate);
}).toThrowWithMessage(
RangeError,
"Cannot format Temporal.PlainDate with calendar 'gregory' in locale with calendar 'iso8601'"
);
expect(() => {
const plainYearMonth = new Temporal.PlainYearMonth(1972, 1, "gregory");
formatter.formatRangeToParts(plainYearMonth, plainYearMonth);
}).toThrowWithMessage(
RangeError,
"Cannot format Temporal.PlainYearMonth with calendar 'gregory' in locale with calendar 'iso8601'"
);
expect(() => {
const plainMonthDay = new Temporal.PlainMonthDay(1, 1, "gregory");
formatter.formatRangeToParts(plainMonthDay, plainMonthDay);
}).toThrowWithMessage(
RangeError,
"Cannot format Temporal.PlainMonthDay with calendar 'gregory' in locale with calendar 'iso8601'"
);
expect(() => {
const plainDateTime = new Temporal.PlainDateTime(
1972,
1,
1,
8,
45,
56,
123,
345,
789,
"gregory"
);
formatter.formatRangeToParts(plainDateTime, plainDateTime);
}).toThrowWithMessage(
RangeError,
"Cannot format Temporal.PlainDateTime with calendar 'gregory' in locale with calendar 'iso8601'"
);
});
test("cannot format Temporal.ZonedDateTime", () => {
expect(() => {
const zonedDateTime = new Temporal.ZonedDateTime(0n, "UTC");
new Intl.DateTimeFormat().formatRangeToParts(zonedDateTime, zonedDateTime);
}).toThrowWithMessage(
TypeError,
"Cannot format Temporal.ZonedDateTime, use Temporal.ZonedDateTime.prototype.toLocaleString"
);
});
test("cannot mix Temporal object types", () => {
expect(() => {
const plainDate = new Temporal.PlainDate(1972, 1, 1, "gregory");
new Intl.DateTimeFormat().formatRangeToParts(plainDate, 0);
}).toThrowWithMessage(
TypeError,
"Cannot format a date-time range with different date-time types"
);
expect(() => {
const plainYearMonth = new Temporal.PlainYearMonth(1972, 1, "gregory");
const plainMonthDay = new Temporal.PlainMonthDay(1, 1, "gregory");
new Intl.DateTimeFormat().formatRangeToParts(plainYearMonth, plainMonthDay);
}).toThrowWithMessage(
TypeError,
"Cannot format a date-time range with different date-time types"
);
});
});
const d0 = Date.UTC(1989, 0, 23, 7, 8, 9, 45);
@ -633,3 +709,112 @@ describe("timeStyle", () => {
]);
});
});
describe("Temporal objects", () => {
const formatter = new Intl.DateTimeFormat("en", {
calendar: "iso8601",
timeZone: "UTC",
});
test("Temporal.PlainDate", () => {
const plainDate1 = new Temporal.PlainDate(1989, 1, 23);
const plainDate2 = new Temporal.PlainDate(2024, 11, 27);
expect(formatter.formatRangeToParts(plainDate1, plainDate2)).toEqual([
{ type: "month", value: "1", source: "startRange" },
{ type: "literal", value: "/", source: "startRange" },
{ type: "day", value: "23", source: "startRange" },
{ type: "literal", value: "/", source: "startRange" },
{ type: "year", value: "1989", source: "startRange" },
{ type: "literal", value: " ", source: "shared" },
{ type: "month", value: "11", source: "endRange" },
{ type: "literal", value: "/", source: "endRange" },
{ type: "day", value: "27", source: "endRange" },
{ type: "literal", value: "/", source: "endRange" },
{ type: "year", value: "2024", source: "endRange" },
]);
});
test("Temporal.PlainYearMonth", () => {
const plainYearMonth1 = new Temporal.PlainYearMonth(1989, 1);
const plainYearMonth2 = new Temporal.PlainYearMonth(2024, 11);
expect(formatter.formatRangeToParts(plainYearMonth1, plainYearMonth2)).toEqual([
{ type: "month", value: "1", source: "startRange" },
{ type: "literal", value: "/", source: "startRange" },
{ type: "year", value: "1989", source: "startRange" },
{ type: "literal", value: " ", source: "shared" },
{ type: "month", value: "11", source: "endRange" },
{ type: "literal", value: "/", source: "endRange" },
{ type: "year", value: "2024", source: "endRange" },
]);
});
test("Temporal.PlainMonthDay", () => {
const plainMonthDay1 = new Temporal.PlainMonthDay(1, 23);
const plainMonthDay2 = new Temporal.PlainMonthDay(11, 27);
expect(formatter.formatRangeToParts(plainMonthDay1, plainMonthDay2)).toEqual([
{ type: "month", value: "1", source: "startRange" },
{ type: "literal", value: "/", source: "startRange" },
{ type: "day", value: "23", source: "startRange" },
{ type: "literal", value: " ", source: "shared" },
{ type: "month", value: "11", source: "endRange" },
{ type: "literal", value: "/", source: "endRange" },
{ type: "day", value: "27", source: "endRange" },
]);
});
test("Temporal.PlainTime", () => {
const plainTime1 = new Temporal.PlainTime(8, 10, 51);
const plainTime2 = new Temporal.PlainTime(20, 41, 9);
expect(formatter.formatRangeToParts(plainTime1, plainTime2)).toEqual([
{ type: "hour", value: "8", source: "startRange" },
{ type: "literal", value: ":", source: "startRange" },
{ type: "minute", value: "10", source: "startRange" },
{ type: "literal", value: ":", source: "startRange" },
{ type: "second", value: "51", source: "startRange" },
{ type: "literal", value: " ", source: "startRange" },
{ type: "dayPeriod", value: "AM", source: "startRange" },
{ type: "literal", value: " ", source: "shared" },
{ type: "hour", value: "8", source: "endRange" },
{ type: "literal", value: ":", source: "endRange" },
{ type: "minute", value: "41", source: "endRange" },
{ type: "literal", value: ":", source: "endRange" },
{ type: "second", value: "09", source: "endRange" },
{ type: "literal", value: " ", source: "endRange" },
{ type: "dayPeriod", value: "PM", source: "endRange" },
]);
});
test("Temporal.Instant", () => {
const instant1 = new Temporal.Instant(601546251000000000n);
const instant2 = new Temporal.Instant(1732740069000000000n);
expect(formatter.formatRangeToParts(instant1, instant2)).toEqual([
{ type: "month", value: "1", source: "startRange" },
{ type: "literal", value: "/", source: "startRange" },
{ type: "day", value: "23", source: "startRange" },
{ type: "literal", value: "/", source: "startRange" },
{ type: "year", value: "1989", source: "startRange" },
{ type: "literal", value: ", ", source: "startRange" },
{ type: "hour", value: "8", source: "startRange" },
{ type: "literal", value: ":", source: "startRange" },
{ type: "minute", value: "10", source: "startRange" },
{ type: "literal", value: ":", source: "startRange" },
{ type: "second", value: "51", source: "startRange" },
{ type: "literal", value: " ", source: "startRange" },
{ type: "dayPeriod", value: "AM", source: "startRange" },
{ type: "literal", value: " ", source: "shared" },
{ type: "month", value: "11", source: "endRange" },
{ type: "literal", value: "/", source: "endRange" },
{ type: "day", value: "27", source: "endRange" },
{ type: "literal", value: "/", source: "endRange" },
{ type: "year", value: "2024", source: "endRange" },
{ type: "literal", value: ", ", source: "endRange" },
{ type: "hour", value: "8", source: "endRange" },
{ type: "literal", value: ":", source: "endRange" },
{ type: "minute", value: "41", source: "endRange" },
{ type: "literal", value: ":", source: "endRange" },
{ type: "second", value: "09", source: "endRange" },
{ type: "literal", value: " ", source: "endRange" },
{ type: "dayPeriod", value: "PM", source: "endRange" },
]);
});
});

View file

@ -28,6 +28,49 @@ describe("errors", () => {
Intl.DateTimeFormat().formatToParts(8.65e15);
}).toThrowWithMessage(RangeError, "Time value must be between -8.64E15 and 8.64E15");
});
test("Temporal object must have same calendar", () => {
const formatter = new Intl.DateTimeFormat([], { calendar: "iso8601" });
expect(() => {
formatter.formatToParts(new Temporal.PlainDate(1972, 1, 1, "gregory"));
}).toThrowWithMessage(
RangeError,
"Cannot format Temporal.PlainDate with calendar 'gregory' in locale with calendar 'iso8601'"
);
expect(() => {
formatter.formatToParts(new Temporal.PlainYearMonth(1972, 1, "gregory"));
}).toThrowWithMessage(
RangeError,
"Cannot format Temporal.PlainYearMonth with calendar 'gregory' in locale with calendar 'iso8601'"
);
expect(() => {
formatter.formatToParts(new Temporal.PlainMonthDay(1, 1, "gregory"));
}).toThrowWithMessage(
RangeError,
"Cannot format Temporal.PlainMonthDay with calendar 'gregory' in locale with calendar 'iso8601'"
);
expect(() => {
formatter.formatToParts(
new Temporal.PlainDateTime(1972, 1, 1, 8, 45, 56, 123, 345, 789, "gregory")
);
}).toThrowWithMessage(
RangeError,
"Cannot format Temporal.PlainDateTime with calendar 'gregory' in locale with calendar 'iso8601'"
);
});
test("cannot format Temporal.ZonedDateTime", () => {
expect(() => {
new Intl.DateTimeFormat().formatToParts(new Temporal.ZonedDateTime(0n, "UTC"));
}).toThrowWithMessage(
TypeError,
"Cannot format Temporal.ZonedDateTime, use Temporal.ZonedDateTime.prototype.toLocaleString"
);
});
});
const d = Date.UTC(1989, 0, 23, 7, 8, 9, 45);
@ -275,3 +318,71 @@ describe("special cases", () => {
]);
});
});
describe("Temporal objects", () => {
const formatter = new Intl.DateTimeFormat("en", {
calendar: "iso8601",
timeZone: "UTC",
});
test("Temporal.PlainDate", () => {
const plainDate = new Temporal.PlainDate(1989, 1, 23);
expect(formatter.formatToParts(plainDate)).toEqual([
{ type: "month", value: "1" },
{ type: "literal", value: "/" },
{ type: "day", value: "23" },
{ type: "literal", value: "/" },
{ type: "year", value: "1989" },
]);
});
test("Temporal.PlainYearMonth", () => {
const plainYearMonth = new Temporal.PlainYearMonth(1989, 1);
expect(formatter.formatToParts(plainYearMonth)).toEqual([
{ type: "month", value: "1" },
{ type: "literal", value: "/" },
{ type: "year", value: "1989" },
]);
});
test("Temporal.PlainMonthDay", () => {
const plainMonthDay = new Temporal.PlainMonthDay(1, 23);
expect(formatter.formatToParts(plainMonthDay)).toEqual([
{ type: "month", value: "1" },
{ type: "literal", value: "/" },
{ type: "day", value: "23" },
]);
});
test("Temporal.PlainTime", () => {
const plainTime = new Temporal.PlainTime(8, 10, 51);
expect(formatter.formatToParts(plainTime)).toEqual([
{ type: "hour", value: "8" },
{ type: "literal", value: ":" },
{ type: "minute", value: "10" },
{ type: "literal", value: ":" },
{ type: "second", value: "51" },
{ type: "literal", value: " " },
{ type: "dayPeriod", value: "AM" },
]);
});
test("Temporal.Instant", () => {
const instant = new Temporal.Instant(1732740069000000000n);
expect(formatter.formatToParts(instant)).toEqual([
{ type: "month", value: "11" },
{ type: "literal", value: "/" },
{ type: "day", value: "27" },
{ type: "literal", value: "/" },
{ type: "year", value: "2024" },
{ type: "literal", value: ", " },
{ type: "hour", value: "8" },
{ type: "literal", value: ":" },
{ type: "minute", value: "41" },
{ type: "literal", value: ":" },
{ type: "second", value: "09" },
{ type: "literal", value: " " },
{ type: "dayPeriod", value: "PM" },
]);
});
});