From 415742ab983f04c4c04747f745178b85307b1747 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Tue, 26 Jul 2022 06:38:35 -0400 Subject: [PATCH] LibJS: Allow out-of-order date ranges to be formatted This is a normative change to the Intl spec: https://github.com/tc39/ecma402/commit/769df4b --- Userland/Libraries/LibJS/Runtime/ErrorTypes.h | 1 - .../LibJS/Runtime/Intl/DateTimeFormat.cpp | 28 +++++------ .../DateTimeFormat.prototype.formatRange.js | 14 +++--- ...TimeFormat.prototype.formatRangeToParts.js | 46 ++++++++++++++++--- 4 files changed, 60 insertions(+), 29 deletions(-) diff --git a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h index 4d69be9572..2565a973d1 100644 --- a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h @@ -49,7 +49,6 @@ M(IntlInvalidTime, "Time value must be between -8.64E15 and 8.64E15") \ M(IntlInvalidUnit, "Unit {} is not a valid time unit") \ M(IntlStartRangeAfterEndRange, "Range start {} is greater than range end {}") \ - M(IntlStartTimeAfterEndTime, "Start time {} is after end time {}") \ M(IntlMinimumExceedsMaximum, "Minimum value {} is larger than maximum value {}") \ M(IntlNumberIsNaN, "{} must not be NaN") \ M(IntlNumberIsNaNOrInfinity, "Number must not be NaN or Infinity") \ diff --git a/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormat.cpp b/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormat.cpp index 18dce8b980..20a17cafb6 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormat.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormat.cpp @@ -934,29 +934,25 @@ ThrowCompletionOr> partition_date_time_range_ if (isnan(end)) return vm.throw_completion(global_object, ErrorType::IntlInvalidTime); - // 5. If x is greater than y, throw a RangeError exception. - if (start > end) - return vm.throw_completion(global_object, ErrorType::IntlStartTimeAfterEndTime, start, end); - - // 6. Let tm1 be ToLocalTime(x, dateTimeFormat.[[Calendar]], dateTimeFormat.[[TimeZone]]). + // 5. Let tm1 be ToLocalTime(x, dateTimeFormat.[[Calendar]], dateTimeFormat.[[TimeZone]]). auto start_local_time = TRY(to_local_time(global_object, start, date_time_format.calendar(), date_time_format.time_zone())); - // 7. Let tm2 be ToLocalTime(y, dateTimeFormat.[[Calendar]], dateTimeFormat.[[TimeZone]]). + // 6. Let tm2 be ToLocalTime(y, dateTimeFormat.[[Calendar]], dateTimeFormat.[[TimeZone]]). auto end_local_time = TRY(to_local_time(global_object, end, date_time_format.calendar(), date_time_format.time_zone())); - // 8. Let rangePatterns be dateTimeFormat.[[RangePatterns]]. + // 7. Let rangePatterns be dateTimeFormat.[[RangePatterns]]. auto range_patterns = date_time_format.range_patterns(); - // 9. Let rangePattern be undefined. + // 8. Let rangePattern be undefined. Optional range_pattern; - // 10. Let dateFieldsPracticallyEqual be true. + // 9. Let dateFieldsPracticallyEqual be true. bool date_fields_practically_equal = true; - // 11. Let patternContainsLargerDateField be false. + // 10. Let patternContainsLargerDateField be false. bool pattern_contains_larger_date_field = false; - // 12. While dateFieldsPracticallyEqual is true and patternContainsLargerDateField is false, repeat for each row of Table 4 in order, except the header row: + // 11. While dateFieldsPracticallyEqual is true and patternContainsLargerDateField is false, repeat for each row of Table 4 in order, except the header row: for_each_range_pattern_field(start_local_time, end_local_time, [&](auto start_value, auto end_value, auto field_name) { // a. Let fieldName be the name given in the Range Pattern Field column of the row. @@ -1061,7 +1057,7 @@ ThrowCompletionOr> partition_date_time_range_ return IterationDecision::Break; }); - // 13. If dateFieldsPracticallyEqual is true, then + // 12. If dateFieldsPracticallyEqual is true, then if (date_fields_practically_equal) { // a. Let pattern be dateTimeFormat.[[Pattern]]. auto const& pattern = date_time_format.pattern(); @@ -1083,10 +1079,10 @@ ThrowCompletionOr> partition_date_time_range_ return result; } - // 14. Let result be a new empty List. + // 13. Let result be a new empty List. Vector result; - // 15. If rangePattern is undefined, then + // 14. If rangePattern is undefined, then if (!range_pattern.has_value()) { // a. Let rangePattern be rangePatterns.[[Default]]. range_pattern = Unicode::get_calendar_default_range_format(date_time_format.data_locale(), date_time_format.calendar()); @@ -1112,7 +1108,7 @@ ThrowCompletionOr> partition_date_time_range_ // step 3 here: https://unicode.org/reports/tr35/tr35-dates.html#intervalFormats } - // 16. For each Record { [[Pattern]], [[Source]] } rangePatternPart in rangePattern.[[PatternParts]], do + // 15. For each Record { [[Pattern]], [[Source]] } rangePatternPart in rangePattern.[[PatternParts]], do TRY(for_each_range_pattern_with_source(*range_pattern, [&](auto const& pattern, auto source) -> ThrowCompletionOr { // a. Let pattern be rangePatternPart.[[Pattern]]. // b. Let source be rangePatternPart.[[Source]]. @@ -1141,7 +1137,7 @@ ThrowCompletionOr> partition_date_time_range_ return {}; })); - // 17. Return result. + // 16. Return result. return result; } diff --git a/Userland/Libraries/LibJS/Tests/builtins/Intl/DateTimeFormat/DateTimeFormat.prototype.formatRange.js b/Userland/Libraries/LibJS/Tests/builtins/Intl/DateTimeFormat/DateTimeFormat.prototype.formatRange.js index 777bfccdc8..c703ee31f6 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/Intl/DateTimeFormat/DateTimeFormat.prototype.formatRange.js +++ b/Userland/Libraries/LibJS/Tests/builtins/Intl/DateTimeFormat/DateTimeFormat.prototype.formatRange.js @@ -36,12 +36,6 @@ describe("errors", () => { }).toThrowWithMessage(RangeError, "Time value must be between -8.64E15 and 8.64E15"); }); }); - - test("called with values in bad order", () => { - expect(() => { - Intl.DateTimeFormat().formatRange(new Date(2021), new Date(1989)); - }).toThrowWithMessage(RangeError, "Start time 2021 is after end time 1989"); - }); }); const d0 = Date.UTC(1989, 0, 23, 7, 8, 9, 45); @@ -157,6 +151,14 @@ describe("dateStyle", () => { expect(ja.formatRange(d0, d1)).toBe(d.ja); }); }); + + test("dates in reverse order", () => { + const en = new Intl.DateTimeFormat("en", { dateStyle: "full", timeZone: "UTC" }); + expect(en.formatRange(d1, d0)).toBe("Tuesday, December 7, 2021 – Monday, January 23, 1989"); + + const ja = new Intl.DateTimeFormat("ja", { dateStyle: "full", timeZone: "UTC" }); + expect(ja.formatRange(d1, d0)).toBe("2021年12月7日火曜日~1989年1月23日月曜日"); + }); }); describe("timeStyle", () => { diff --git a/Userland/Libraries/LibJS/Tests/builtins/Intl/DateTimeFormat/DateTimeFormat.prototype.formatRangeToParts.js b/Userland/Libraries/LibJS/Tests/builtins/Intl/DateTimeFormat/DateTimeFormat.prototype.formatRangeToParts.js index 0ced06df86..3fb65f8a36 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/Intl/DateTimeFormat/DateTimeFormat.prototype.formatRangeToParts.js +++ b/Userland/Libraries/LibJS/Tests/builtins/Intl/DateTimeFormat/DateTimeFormat.prototype.formatRangeToParts.js @@ -36,12 +36,6 @@ describe("errors", () => { }).toThrowWithMessage(RangeError, "Time value must be between -8.64E15 and 8.64E15"); }); }); - - test("called with values in bad order", () => { - expect(() => { - Intl.DateTimeFormat().formatRangeToParts(new Date(2021), new Date(1989)); - }).toThrowWithMessage(RangeError, "Start time 2021 is after end time 1989"); - }); }); const d0 = Date.UTC(1989, 0, 23, 7, 8, 9, 45); @@ -343,6 +337,46 @@ describe("dateStyle", () => { { type: "day", value: "07", source: "endRange" }, ]); }); + + test("dates in reverse order", () => { + const en = new Intl.DateTimeFormat("en", { dateStyle: "full", timeZone: "UTC" }); + expect(en.formatRangeToParts(d1, d0)).toEqual([ + { type: "weekday", value: "Tuesday", source: "startRange" }, + { type: "literal", value: ", ", source: "startRange" }, + { type: "month", value: "December", source: "startRange" }, + { type: "literal", value: " ", source: "startRange" }, + { type: "day", value: "7", source: "startRange" }, + { type: "literal", value: ", ", source: "startRange" }, + { type: "year", value: "2021", source: "startRange" }, + { type: "literal", value: " – ", source: "shared" }, + { type: "weekday", value: "Monday", source: "endRange" }, + { type: "literal", value: ", ", source: "endRange" }, + { type: "month", value: "January", source: "endRange" }, + { type: "literal", value: " ", source: "endRange" }, + { type: "day", value: "23", source: "endRange" }, + { type: "literal", value: ", ", source: "endRange" }, + { type: "year", value: "1989", source: "endRange" }, + ]); + + const ja = new Intl.DateTimeFormat("ja", { dateStyle: "full", timeZone: "UTC" }); + expect(ja.formatRangeToParts(d1, d0)).toEqual([ + { type: "year", value: "2021", source: "startRange" }, + { type: "literal", value: "年", source: "startRange" }, + { type: "month", value: "12", source: "startRange" }, + { type: "literal", value: "月", source: "startRange" }, + { type: "day", value: "7", source: "startRange" }, + { type: "literal", value: "日", source: "startRange" }, + { type: "weekday", value: "火曜日", source: "startRange" }, + { type: "literal", value: "~", source: "shared" }, + { type: "year", value: "1989", source: "endRange" }, + { type: "literal", value: "年", source: "endRange" }, + { type: "month", value: "1", source: "endRange" }, + { type: "literal", value: "月", source: "endRange" }, + { type: "day", value: "23", source: "endRange" }, + { type: "literal", value: "日", source: "endRange" }, + { type: "weekday", value: "月曜日", source: "endRange" }, + ]); + }); }); describe("timeStyle", () => {