From fd7d97fba53ae2133b754c3806e1f63a599ffbd2 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Tue, 26 Jul 2022 06:55:25 -0400 Subject: [PATCH] LibJS: Allow out-of-order number ranges to be formatted This is a normative change to the Intl NumberFormat V3 spec: https://github.com/tc39/proposal-intl-numberformat-v3/commit/0c3d849 --- Userland/Libraries/LibJS/Runtime/ErrorTypes.h | 1 - .../LibJS/Runtime/Intl/NumberFormat.cpp | 61 +++----------- .../NumberFormat.prototype.formatRange.js | 57 ++++++------- ...mberFormat.prototype.formatRangeToParts.js | 79 +++++++++++-------- 4 files changed, 77 insertions(+), 121 deletions(-) diff --git a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h index 2565a973d1..4d58d04919 100644 --- a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h @@ -53,7 +53,6 @@ M(IntlNumberIsNaN, "{} must not be NaN") \ M(IntlNumberIsNaNOrInfinity, "Number must not be NaN or Infinity") \ M(IntlNumberIsNaNOrOutOfRange, "Value {} is NaN or is not between {} and {}") \ - M(IntlNumberRangeIsInvalid, "Numeric range is invalid: {}") \ M(IntlOptionUndefined, "Option {} must be defined when option {} is {}") \ M(IntlNonNumericOr2DigitAfterNumericOr2Digit, "Styles other than 'numeric' and '2-digit' may not be used in smaller units after " \ "being used in larger units") \ diff --git a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp index b71ce35c89..f407b91cd3 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp @@ -1729,90 +1729,51 @@ ThrowCompletionOr> partition_number_range_pat if (end.is_nan()) return vm.throw_completion(global_object, ErrorType::IntlNumberIsNaN, "end"sv); - // 2. If x is a mathematical value, then - if (start.is_mathematical_value()) { - // a. If y is a mathematical value and y < x, throw a RangeError exception. - if (end.is_mathematical_value() && end.is_less_than(start)) - return vm.throw_completion(global_object, ErrorType::IntlNumberRangeIsInvalid, "start is a mathematical value, end is a mathematical value and end < start"sv); - - // b. Else if y is -∞, throw a RangeError exception. - if (end.is_negative_infinity()) - return vm.throw_completion(global_object, ErrorType::IntlNumberRangeIsInvalid, "start is a mathematical value, end is -∞"sv); - - // c. Else if y is -0𝔽 and x ≥ 0, throw a RangeError exception. - if (end.is_negative_zero() && (start.is_zero() || start.is_positive())) - return vm.throw_completion(global_object, ErrorType::IntlNumberRangeIsInvalid, "start is a mathematical value, end is -0 and start ≥ 0"sv); - } - // 3. Else if x is +∞, then - else if (start.is_positive_infinity()) { - // a. If y is a mathematical value, throw a RangeError exception. - if (end.is_mathematical_value()) - return vm.throw_completion(global_object, ErrorType::IntlNumberRangeIsInvalid, "start is +∞, end is a mathematical value"sv); - - // b. Else if y is -∞, throw a RangeError exception. - if (end.is_negative_infinity()) - return vm.throw_completion(global_object, ErrorType::IntlNumberRangeIsInvalid, "start is +∞, end is -∞"sv); - - // c. Else if y is -0𝔽, throw a RangeError exception. - if (end.is_negative_zero()) - return vm.throw_completion(global_object, ErrorType::IntlNumberRangeIsInvalid, "start is +∞, end is -0"sv); - } - // 4. Else if x is -0𝔽, then - else if (start.is_negative_zero()) { - // a. If y is a mathematical value and y < 0, throw a RangeError exception. - if (end.is_mathematical_value() && end.is_negative()) - return vm.throw_completion(global_object, ErrorType::IntlNumberRangeIsInvalid, "start is -0, end is a mathematical value and end < 0"sv); - - // b. Else if y is -∞, throw a RangeError exception. - if (end.is_negative_infinity()) - return vm.throw_completion(global_object, ErrorType::IntlNumberRangeIsInvalid, "start is -0, end is -∞"sv); - } - - // 5. Let result be a new empty List. + // 2. Let result be a new empty List. Vector result; - // 6. Let xResult be ? PartitionNumberPattern(numberFormat, x). + // 3. Let xResult be ? PartitionNumberPattern(numberFormat, x). auto raw_start_result = partition_number_pattern(global_object, number_format, move(start)); auto start_result = PatternPartitionWithSource::create_from_parent_list(move(raw_start_result)); - // 7. Let yResult be ? PartitionNumberPattern(numberFormat, y). + // 4. Let yResult be ? PartitionNumberPattern(numberFormat, y). auto raw_end_result = partition_number_pattern(global_object, number_format, move(end)); auto end_result = PatternPartitionWithSource::create_from_parent_list(move(raw_end_result)); - // 8. If xResult is equal to yResult, return FormatApproximately(numberFormat, xResult). + // 5. If xResult is equal to yResult, return FormatApproximately(numberFormat, xResult). if (start_result == end_result) return format_approximately(number_format, move(start_result)); - // 9. For each r in xResult, do + // 6. For each r in xResult, do for (auto& part : start_result) { // i. Set r.[[Source]] to "startRange". part.source = "startRange"sv; } - // 10. Add all elements in xResult to result in order. + // 7. Add all elements in xResult to result in order. result = move(start_result); - // 11. Let rangeSeparator be an ILND String value used to separate two numbers. + // 8. Let rangeSeparator be an ILND String value used to separate two numbers. auto range_separator_symbol = Unicode::get_number_system_symbol(number_format.data_locale(), number_format.numbering_system(), Unicode::NumericSymbol::RangeSeparator).value_or("-"sv); auto range_separator = Unicode::augment_range_pattern(range_separator_symbol, result.last().value, end_result[0].value); - // 12. Append a new Record { [[Type]]: "literal", [[Value]]: rangeSeparator, [[Source]]: "shared" } element to result. + // 9. Append a new Record { [[Type]]: "literal", [[Value]]: rangeSeparator, [[Source]]: "shared" } element to result. PatternPartitionWithSource part; part.type = "literal"sv; part.value = range_separator.value_or(range_separator_symbol); part.source = "shared"sv; result.append(move(part)); - // 13. For each r in yResult, do + // 10. For each r in yResult, do for (auto& part : end_result) { // a. Set r.[[Source]] to "endRange". part.source = "endRange"sv; } - // 14. Add all elements in yResult to result in order. + // 11. Add all elements in yResult to result in order. result.extend(move(end_result)); - // 15. Return ! CollapseNumberRange(result). + // 12. Return ! CollapseNumberRange(result). return collapse_number_range(move(result)); } diff --git a/Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.prototype.formatRange.js b/Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.prototype.formatRange.js index d6fd3438bf..c366e6b8ba 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.prototype.formatRange.js +++ b/Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.prototype.formatRange.js @@ -33,41 +33,6 @@ describe("errors", () => { expect(() => { new Intl.NumberFormat().formatRange(1, NaN); }).toThrowWithMessage(RangeError, "end must not be NaN"); - - expect(() => { - new Intl.NumberFormat().formatRange(1, 0); - }).toThrowWithMessage( - RangeError, - "start is a mathematical value, end is a mathematical value and end < start" - ); - - expect(() => { - new Intl.NumberFormat().formatRange(1, -Infinity); - }).toThrowWithMessage(RangeError, "start is a mathematical value, end is -∞"); - - expect(() => { - new Intl.NumberFormat().formatRange(1, -0); - }).toThrowWithMessage(RangeError, "start is a mathematical value, end is -0 and start ≥ 0"); - - expect(() => { - new Intl.NumberFormat().formatRange(Infinity, 0); - }).toThrowWithMessage(RangeError, "start is +∞, end is a mathematical value"); - - expect(() => { - new Intl.NumberFormat().formatRange(Infinity, -Infinity); - }).toThrowWithMessage(RangeError, "start is +∞, end is -∞"); - - expect(() => { - new Intl.NumberFormat().formatRange(Infinity, -0); - }).toThrowWithMessage(RangeError, "start is +∞, end is -0"); - - expect(() => { - new Intl.NumberFormat().formatRange(-0, -1); - }).toThrowWithMessage(RangeError, "start is -0, end is a mathematical value and end < 0"); - - expect(() => { - new Intl.NumberFormat().formatRange(-0, -Infinity); - }).toThrowWithMessage(RangeError, "start is -0, end is -∞"); }); }); @@ -137,4 +102,26 @@ describe("correct behavior", () => { }); expect(ja2.formatRange(3, 5)).toBe("¥3 ~ ¥5"); }); + + test("numbers in reverse order", () => { + const en = new Intl.NumberFormat("en"); + expect(en.formatRange(1, 0)).toBe("1–0"); + expect(en.formatRange(1, -Infinity)).toBe("1 – -∞"); + expect(en.formatRange(1, -0)).toBe("1 – -0"); + expect(en.formatRange(Infinity, 0)).toBe("∞ – 0"); + expect(en.formatRange(Infinity, -Infinity)).toBe("∞ – -∞"); + expect(en.formatRange(Infinity, -0)).toBe("∞ – -0"); + expect(en.formatRange(-0, -1)).toBe("-0 – -1"); + expect(en.formatRange(-0, -Infinity)).toBe("-0 – -∞"); + + const ja = new Intl.NumberFormat("ja"); + expect(ja.formatRange(1, 0)).toBe("1~0"); + expect(ja.formatRange(1, -Infinity)).toBe("1 ~ -∞"); + expect(ja.formatRange(1, -0)).toBe("1 ~ -0"); + expect(ja.formatRange(Infinity, 0)).toBe("∞ ~ 0"); + expect(ja.formatRange(Infinity, -Infinity)).toBe("∞ ~ -∞"); + expect(ja.formatRange(Infinity, -0)).toBe("∞ ~ -0"); + expect(ja.formatRange(-0, -1)).toBe("-0 ~ -1"); + expect(ja.formatRange(-0, -Infinity)).toBe("-0 ~ -∞"); + }); }); diff --git a/Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.prototype.formatRangeToParts.js b/Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.prototype.formatRangeToParts.js index 0da99fb828..3032d6ec7b 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.prototype.formatRangeToParts.js +++ b/Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.prototype.formatRangeToParts.js @@ -33,41 +33,6 @@ describe("errors", () => { expect(() => { new Intl.NumberFormat().formatRangeToParts(1, NaN); }).toThrowWithMessage(RangeError, "end must not be NaN"); - - expect(() => { - new Intl.NumberFormat().formatRangeToParts(1, 0); - }).toThrowWithMessage( - RangeError, - "start is a mathematical value, end is a mathematical value and end < start" - ); - - expect(() => { - new Intl.NumberFormat().formatRangeToParts(1, -Infinity); - }).toThrowWithMessage(RangeError, "start is a mathematical value, end is -∞"); - - expect(() => { - new Intl.NumberFormat().formatRangeToParts(1, -0); - }).toThrowWithMessage(RangeError, "start is a mathematical value, end is -0 and start ≥ 0"); - - expect(() => { - new Intl.NumberFormat().formatRangeToParts(Infinity, 0); - }).toThrowWithMessage(RangeError, "start is +∞, end is a mathematical value"); - - expect(() => { - new Intl.NumberFormat().formatRangeToParts(Infinity, -Infinity); - }).toThrowWithMessage(RangeError, "start is +∞, end is -∞"); - - expect(() => { - new Intl.NumberFormat().formatRangeToParts(Infinity, -0); - }).toThrowWithMessage(RangeError, "start is +∞, end is -0"); - - expect(() => { - new Intl.NumberFormat().formatRangeToParts(-0, -1); - }).toThrowWithMessage(RangeError, "start is -0, end is a mathematical value and end < 0"); - - expect(() => { - new Intl.NumberFormat().formatRangeToParts(-0, -Infinity); - }).toThrowWithMessage(RangeError, "start is -0, end is -∞"); }); }); @@ -165,4 +130,48 @@ describe("correct behavior", () => { { type: "integer", value: "5", source: "endRange" }, ]); }); + + test("numbers in reverse order", () => { + const en = new Intl.NumberFormat("en"); + expect(en.formatRangeToParts(1, -Infinity)).toEqual([ + { type: "integer", value: "1", source: "startRange" }, + { type: "literal", value: " – ", source: "shared" }, + { type: "minusSign", value: "-", source: "endRange" }, + { type: "infinity", value: "∞", source: "endRange" }, + ]); + expect(en.formatRangeToParts(Infinity, -Infinity)).toEqual([ + { type: "infinity", value: "∞", source: "startRange" }, + { type: "literal", value: " – ", source: "shared" }, + { type: "minusSign", value: "-", source: "endRange" }, + { type: "infinity", value: "∞", source: "endRange" }, + ]); + expect(en.formatRangeToParts(-0, -Infinity)).toEqual([ + { type: "minusSign", value: "-", source: "startRange" }, + { type: "integer", value: "0", source: "startRange" }, + { type: "literal", value: " – ", source: "shared" }, + { type: "minusSign", value: "-", source: "endRange" }, + { type: "infinity", value: "∞", source: "endRange" }, + ]); + + const ja = new Intl.NumberFormat("ja"); + expect(ja.formatRangeToParts(1, -Infinity)).toEqual([ + { type: "integer", value: "1", source: "startRange" }, + { type: "literal", value: " ~ ", source: "shared" }, + { type: "minusSign", value: "-", source: "endRange" }, + { type: "infinity", value: "∞", source: "endRange" }, + ]); + expect(ja.formatRangeToParts(Infinity, -Infinity)).toEqual([ + { type: "infinity", value: "∞", source: "startRange" }, + { type: "literal", value: " ~ ", source: "shared" }, + { type: "minusSign", value: "-", source: "endRange" }, + { type: "infinity", value: "∞", source: "endRange" }, + ]); + expect(ja.formatRangeToParts(-0, -Infinity)).toEqual([ + { type: "minusSign", value: "-", source: "startRange" }, + { type: "integer", value: "0", source: "startRange" }, + { type: "literal", value: " ~ ", source: "shared" }, + { type: "minusSign", value: "-", source: "endRange" }, + { type: "infinity", value: "∞", source: "endRange" }, + ]); + }); });