Browse Source

LibJS: Implement Intl.NumberFormat.prototype.formatRange

Timothy Flynn 3 years ago
parent
commit
b4a772cde2

+ 1 - 0
Userland/Libraries/LibJS/Runtime/ErrorTypes.h

@@ -54,6 +54,7 @@
     M(IntlNumberIsNaN, "{} must not be NaN")                                                                                            \
     M(IntlNumberIsNaN, "{} must not be NaN")                                                                                            \
     M(IntlNumberIsNaNOrInfinity, "Number must not be NaN or Infinity")                                                                  \
     M(IntlNumberIsNaNOrInfinity, "Number must not be NaN or Infinity")                                                                  \
     M(IntlNumberIsNaNOrOutOfRange, "Value {} is NaN or is not between {} and {}")                                                       \
     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(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 "   \
     M(IntlNonNumericOr2DigitAfterNumericOr2Digit, "Styles other than 'numeric' and '2-digit' may not be used in smaller units after "   \
                                                   "being used in larger units")                                                         \
                                                   "being used in larger units")                                                         \

+ 144 - 0
Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp

@@ -1718,4 +1718,148 @@ RoundingDecision apply_unsigned_rounding_mode(MathematicalValue const& x, Mathem
     return RoundingDecision::HigherValue;
     return RoundingDecision::HigherValue;
 }
 }
 
 
+// 1.1.21 PartitionNumberRangePattern ( numberFormat, x, y ), https://tc39.es/proposal-intl-numberformat-v3/out/numberformat/proposed.html#sec-partitionnumberrangepattern
+ThrowCompletionOr<Vector<PatternPartitionWithSource>> partition_number_range_pattern(GlobalObject& global_object, NumberFormat& number_format, MathematicalValue start, MathematicalValue end)
+{
+    auto& vm = global_object.vm();
+
+    // 1. If x is NaN or y is NaN, throw a RangeError exception.
+    if (start.is_nan())
+        return vm.throw_completion<RangeError>(global_object, ErrorType::IntlNumberIsNaN, "start"sv);
+    if (end.is_nan())
+        return vm.throw_completion<RangeError>(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<RangeError>(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<RangeError>(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<RangeError>(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<RangeError>(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<RangeError>(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<RangeError>(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<RangeError>(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<RangeError>(global_object, ErrorType::IntlNumberRangeIsInvalid, "start is -0, end is -∞"sv);
+    }
+
+    // 5. Let result be a new empty List.
+    Vector<PatternPartitionWithSource> result;
+
+    // 6. 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).
+    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).
+    if (start_result == end_result)
+        return format_approximately(number_format, move(start_result));
+
+    // 9. 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.
+    result = move(start_result);
+
+    // 11. 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.
+    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
+    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.
+    result.extend(move(end_result));
+
+    // 15. Return ! CollapseNumberRange(result).
+    return collapse_number_range(move(result));
+}
+
+// 1.1.22 FormatApproximately ( numberFormat, result ), https://tc39.es/proposal-intl-numberformat-v3/out/numberformat/proposed.html#sec-formatapproximately
+Vector<PatternPartitionWithSource> format_approximately(NumberFormat& number_format, Vector<PatternPartitionWithSource> result)
+{
+    // 1. Let i be an index into result, determined by an implementation-defined algorithm based on numberFormat and result.
+    // 2. Let approximatelySign be an ILND String value used to signify that a number is approximate.
+    auto approximately_sign = Unicode::get_number_system_symbol(number_format.data_locale(), number_format.numbering_system(), Unicode::NumericSymbol::ApproximatelySign).value_or("~"sv);
+
+    // 3. Insert a new Record { [[Type]]: "approximatelySign", [[Value]]: approximatelySign } at index i in result.
+    PatternPartitionWithSource partition;
+    partition.type = "approximatelySign"sv;
+    partition.value = approximately_sign;
+
+    result.insert_before_matching(move(partition), [](auto const& part) {
+        return part.type.is_one_of("integer"sv, "decimal"sv, "plusSign"sv, "minusSign"sv, "percentSign"sv, "currency"sv);
+    });
+
+    // 4. Return result.
+    return result;
+}
+
+// 1.1.23 CollapseNumberRange ( result ), https://tc39.es/proposal-intl-numberformat-v3/out/numberformat/proposed.html#sec-collapsenumberrange
+Vector<PatternPartitionWithSource> collapse_number_range(Vector<PatternPartitionWithSource> result)
+{
+    // Returning result unmodified is guaranteed to be a correct implementation of CollapseNumberRange.
+    return result;
+}
+
+// 1.1.24 FormatNumericRange( numberFormat, x, y ), https://tc39.es/proposal-intl-numberformat-v3/out/numberformat/proposed.html#sec-formatnumericrange
+ThrowCompletionOr<String> format_numeric_range(GlobalObject& global_object, NumberFormat& number_format, MathematicalValue start, MathematicalValue end)
+{
+    // 1. Let parts be ? PartitionNumberRangePattern(numberFormat, x, y).
+    auto parts = TRY(partition_number_range_pattern(global_object, number_format, move(start), move(end)));
+
+    // 2. Let result be the empty String.
+    StringBuilder result;
+
+    // 3. For each part in parts, do
+    for (auto& part : parts) {
+        // a. Set result to the string-concatenation of result and part.[[Value]].
+        result.append(move(part.value));
+    }
+
+    // 4. Return result.
+    return result.build();
+}
+
 }
 }

+ 4 - 0
Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.h

@@ -287,5 +287,9 @@ int compute_exponent_for_magnitude(NumberFormat& number_format, int magnitude);
 ThrowCompletionOr<MathematicalValue> to_intl_mathematical_value(GlobalObject& global_object, Value value);
 ThrowCompletionOr<MathematicalValue> to_intl_mathematical_value(GlobalObject& global_object, Value value);
 NumberFormat::UnsignedRoundingMode get_unsigned_rounding_mode(NumberFormat::RoundingMode rounding_mode, bool is_negative);
 NumberFormat::UnsignedRoundingMode get_unsigned_rounding_mode(NumberFormat::RoundingMode rounding_mode, bool is_negative);
 RoundingDecision apply_unsigned_rounding_mode(MathematicalValue const& x, MathematicalValue const& r1, MathematicalValue const& r2, Optional<NumberFormat::UnsignedRoundingMode> const& unsigned_rounding_mode);
 RoundingDecision apply_unsigned_rounding_mode(MathematicalValue const& x, MathematicalValue const& r1, MathematicalValue const& r2, Optional<NumberFormat::UnsignedRoundingMode> const& unsigned_rounding_mode);
+ThrowCompletionOr<Vector<PatternPartitionWithSource>> partition_number_range_pattern(GlobalObject& global_object, NumberFormat& number_format, MathematicalValue start, MathematicalValue end);
+Vector<PatternPartitionWithSource> format_approximately(NumberFormat& number_format, Vector<PatternPartitionWithSource> result);
+Vector<PatternPartitionWithSource> collapse_number_range(Vector<PatternPartitionWithSource> result);
+ThrowCompletionOr<String> format_numeric_range(GlobalObject& global_object, NumberFormat& number_format, MathematicalValue start, MathematicalValue end);
 
 
 }
 }

+ 28 - 0
Userland/Libraries/LibJS/Runtime/Intl/NumberFormatPrototype.cpp

@@ -32,6 +32,7 @@ void NumberFormatPrototype::initialize(GlobalObject& global_object)
 
 
     u8 attr = Attribute::Writable | Attribute::Configurable;
     u8 attr = Attribute::Writable | Attribute::Configurable;
     define_native_function(vm.names.formatToParts, format_to_parts, 1, attr);
     define_native_function(vm.names.formatToParts, format_to_parts, 1, attr);
+    define_native_function(vm.names.formatRange, format_range, 2, attr);
     define_native_function(vm.names.resolvedOptions, resolved_options, 0, attr);
     define_native_function(vm.names.resolvedOptions, resolved_options, 0, attr);
 }
 }
 
 
@@ -76,6 +77,33 @@ JS_DEFINE_NATIVE_FUNCTION(NumberFormatPrototype::format_to_parts)
     return format_numeric_to_parts(global_object, *number_format, move(mathematical_value));
     return format_numeric_to_parts(global_object, *number_format, move(mathematical_value));
 }
 }
 
 
+// 1.4.5 Intl.NumberFormat.prototype.formatRange ( start, end ), https://tc39.es/proposal-intl-numberformat-v3/out/numberformat/proposed.html#sec-intl.numberformat.prototype.formatrange
+JS_DEFINE_NATIVE_FUNCTION(NumberFormatPrototype::format_range)
+{
+    auto start = vm.argument(0);
+    auto end = vm.argument(1);
+
+    // 1. Let nf be the this value.
+    // 2. Perform ? RequireInternalSlot(nf, [[InitializedNumberFormat]]).
+    auto* number_format = TRY(typed_this_object(global_object));
+
+    // 3. If start is undefined or end is undefined, throw a TypeError exception.
+    if (start.is_undefined())
+        return vm.throw_completion<TypeError>(global_object, ErrorType::IsUndefined, "start"sv);
+    if (end.is_undefined())
+        return vm.throw_completion<TypeError>(global_object, ErrorType::IsUndefined, "end"sv);
+
+    // 4. Let x be ? ToIntlMathematicalValue(start).
+    auto x = TRY(to_intl_mathematical_value(global_object, start));
+
+    // 5. Let y be ? ToIntlMathematicalValue(end).
+    auto y = TRY(to_intl_mathematical_value(global_object, end));
+
+    // 6. Return ? FormatNumericRange(nf, x, y).
+    auto formatted = TRY(format_numeric_range(global_object, *number_format, move(x), move(y)));
+    return js_string(vm, move(formatted));
+}
+
 // 15.3.5 Intl.NumberFormat.prototype.resolvedOptions ( ), https://tc39.es/ecma402/#sec-intl.numberformat.prototype.resolvedoptions
 // 15.3.5 Intl.NumberFormat.prototype.resolvedOptions ( ), https://tc39.es/ecma402/#sec-intl.numberformat.prototype.resolvedoptions
 JS_DEFINE_NATIVE_FUNCTION(NumberFormatPrototype::resolved_options)
 JS_DEFINE_NATIVE_FUNCTION(NumberFormatPrototype::resolved_options)
 {
 {

+ 1 - 0
Userland/Libraries/LibJS/Runtime/Intl/NumberFormatPrototype.h

@@ -22,6 +22,7 @@ public:
 private:
 private:
     JS_DECLARE_NATIVE_FUNCTION(format);
     JS_DECLARE_NATIVE_FUNCTION(format);
     JS_DECLARE_NATIVE_FUNCTION(format_to_parts);
     JS_DECLARE_NATIVE_FUNCTION(format_to_parts);
+    JS_DECLARE_NATIVE_FUNCTION(format_range);
     JS_DECLARE_NATIVE_FUNCTION(resolved_options);
     JS_DECLARE_NATIVE_FUNCTION(resolved_options);
 };
 };
 
 

+ 140 - 0
Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.prototype.formatRange.js

@@ -0,0 +1,140 @@
+describe("errors", () => {
+    test("called on non-NumberFormat object", () => {
+        expect(() => {
+            Intl.NumberFormat.prototype.formatRange();
+        }).toThrowWithMessage(TypeError, "Not an object of type Intl.NumberFormat");
+    });
+
+    test("called without enough values", () => {
+        expect(() => {
+            new Intl.NumberFormat().formatRange();
+        }).toThrowWithMessage(TypeError, "start is undefined");
+
+        expect(() => {
+            new Intl.NumberFormat().formatRange(1);
+        }).toThrowWithMessage(TypeError, "end is undefined");
+    });
+
+    test("called with values that cannot be converted to numbers", () => {
+        expect(() => {
+            new Intl.NumberFormat().formatRange(Symbol.hasInstance, 1);
+        }).toThrowWithMessage(TypeError, "Cannot convert symbol to number");
+
+        expect(() => {
+            new Intl.NumberFormat().formatRange(1, Symbol.hasInstance);
+        }).toThrowWithMessage(TypeError, "Cannot convert symbol to number");
+    });
+
+    test("called with invalid numbers", () => {
+        expect(() => {
+            new Intl.NumberFormat().formatRange(NaN, 1);
+        }).toThrowWithMessage(RangeError, "start must not be NaN");
+
+        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 -∞");
+    });
+});
+
+describe("correct behavior", () => {
+    test("basic functionality", () => {
+        const en1 = new Intl.NumberFormat("en");
+        expect(en1.formatRange(100, 101)).toBe("100–101");
+        expect(en1.formatRange(3.14, 6.28)).toBe("3.14–6.28");
+        expect(en1.formatRange(-0, 1)).toBe("-0–1");
+
+        const ja1 = new Intl.NumberFormat("ja");
+        expect(ja1.formatRange(100, 101)).toBe("100~101");
+        expect(ja1.formatRange(3.14, 6.28)).toBe("3.14~6.28");
+        expect(ja1.formatRange(-0, 1)).toBe("-0~1");
+    });
+
+    test("approximately formatting", () => {
+        const en1 = new Intl.NumberFormat("en", { maximumFractionDigits: 0 });
+        expect(en1.formatRange(2.9, 3.1)).toBe("~3");
+        expect(en1.formatRange(-3.1, -2.9)).toBe("~-3");
+
+        const en2 = new Intl.NumberFormat("en", {
+            style: "currency",
+            currency: "USD",
+            maximumFractionDigits: 0,
+        });
+        expect(en2.formatRange(2.9, 3.1)).toBe("~$3");
+        expect(en2.formatRange(-3.1, -2.9)).toBe("~-$3");
+
+        const ja1 = new Intl.NumberFormat("ja", { maximumFractionDigits: 0 });
+        expect(ja1.formatRange(2.9, 3.1)).toBe("約3");
+        expect(ja1.formatRange(-3.1, -2.9)).toBe("約-3");
+
+        const ja2 = new Intl.NumberFormat("ja", {
+            style: "currency",
+            currency: "JPY",
+            maximumFractionDigits: 0,
+        });
+        expect(ja2.formatRange(2.9, 3.1)).toBe("約¥3");
+        expect(ja2.formatRange(-3.1, -2.9)).toBe("約-¥3");
+    });
+
+    test("range pattern spacing", () => {
+        const en1 = new Intl.NumberFormat("en");
+        expect(en1.formatRange(3, 5)).toBe("3–5");
+        expect(en1.formatRange(-1, -0)).toBe("-1 – -0");
+        expect(en1.formatRange(0, Infinity)).toBe("0 – ∞");
+        expect(en1.formatRange(-Infinity, 0)).toBe("-∞ – 0");
+
+        const en2 = new Intl.NumberFormat("en", {
+            style: "currency",
+            currency: "USD",
+            maximumFractionDigits: 0,
+        });
+        expect(en2.formatRange(3, 5)).toBe("$3 – $5");
+
+        const ja1 = new Intl.NumberFormat("ja");
+        expect(ja1.formatRange(3, 5)).toBe("3~5");
+        expect(ja1.formatRange(-1, -0)).toBe("-1 ~ -0");
+        expect(ja1.formatRange(0, Infinity)).toBe("0 ~ ∞");
+        expect(ja1.formatRange(-Infinity, 0)).toBe("-∞ ~ 0");
+
+        const ja2 = new Intl.NumberFormat("ja", {
+            style: "currency",
+            currency: "JPY",
+            maximumFractionDigits: 0,
+        });
+        expect(ja2.formatRange(3, 5)).toBe("¥3 ~ ¥5");
+    });
+});