Skip to content

Commit

Permalink
[JSC] Add precise IntlMathematicalValue
Browse files Browse the repository at this point in the history
https://bugs.webkit.org/show_bug.cgi?id=263705
rdar://problem/117535507

Reviewed by Justin Michaud.

This patch implements precise IntlMathematicalValue aligning to the spec[1].

1. We should keep string representation if the string value may not be exactly converted to double.
   And we should pass string directly to ICU when it happens.
2. We should appropriately handle 0x / 0o / 0b patterns.

[1]: https://tc39.es/ecma402/#sec-tointlmathematicalvalue

* JSTests/test262/expectations.yaml:
* Source/JavaScriptCore/runtime/IntlNumberFormat.cpp:
(JSC::IntlMathematicalValue::parseString):
* Source/JavaScriptCore/runtime/IntlNumberFormat.h:
* Source/JavaScriptCore/runtime/IntlNumberFormatInlines.h:
(JSC::toIntlMathematicalValue):

Canonical link: https://commits.webkit.org/269825@main
  • Loading branch information
Constellation committed Oct 26, 2023
1 parent d0d25eb commit c724273
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 19 deletions.
33 changes: 33 additions & 0 deletions JSTests/stress/intl-mathematical-value.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
function shouldBe(actual, expected) {
if (actual !== expected)
throw new Error('bad value: ' + actual);
}

var nf = new Intl.NumberFormat('en-US', {maximumFractionDigits: 20});

shouldBe(nf.format('0x100000'), '1,048,576');
shouldBe(nf.format('0o100000'), '32,768');
shouldBe(nf.format('0b100000'), '32');
shouldBe(nf.format('0X100000'), '1,048,576');
shouldBe(nf.format('0O100000'), '32,768');
shouldBe(nf.format('0B100000'), '32');

shouldBe(nf.format('0x10000z'), 'NaN');
shouldBe(nf.format('0o10000a'), 'NaN');
shouldBe(nf.format('0b100002'), 'NaN');
shouldBe(nf.format('0X10000z'), 'NaN');
shouldBe(nf.format('0O10000a'), 'NaN');
shouldBe(nf.format('0B100002'), 'NaN');

shouldBe(nf.format('0x1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'), '1,013,065,324,433,836,171,511,818,326,096,474,890,383,898,005,918,563,696,288,002,277,756,507,034,036,354,527,929,615,978,746,851,512,277,392,062,160,962,106,733,983,191,180,520,452,956,027,069,051,297,354,415,786,421,338,721,071,661,056');
shouldBe(nf.format('0o1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'), '5,678,427,533,559,428,832,416,592,249,125,035,424,637,823,130,369,672,345,949,142,181,098,744,438,385,921,275,985,867,583,701,277,855,943,457,200,048,954,515,105,739,075,223,552');
shouldBe(nf.format('0b1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'), '178,405,961,588,244,985,132,285,746,181,186,892,047,843,328');
shouldBe(nf.format('0X1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'), '1,013,065,324,433,836,171,511,818,326,096,474,890,383,898,005,918,563,696,288,002,277,756,507,034,036,354,527,929,615,978,746,851,512,277,392,062,160,962,106,733,983,191,180,520,452,956,027,069,051,297,354,415,786,421,338,721,071,661,056');
shouldBe(nf.format('0O1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'), '5,678,427,533,559,428,832,416,592,249,125,035,424,637,823,130,369,672,345,949,142,181,098,744,438,385,921,275,985,867,583,701,277,855,943,457,200,048,954,515,105,739,075,223,552');
shouldBe(nf.format('0B1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'), '178,405,961,588,244,985,132,285,746,181,186,892,047,843,328');

shouldBe(nf.format('-0'), '-0');
shouldBe(nf.format('+0'), '0');
shouldBe(nf.format('-Infinity'), '-∞');
shouldBe(nf.format('+Infinity'), '∞');
shouldBe(nf.format('Infinity'), '∞');
7 changes: 2 additions & 5 deletions JSTests/test262/expectations.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1010,8 +1010,8 @@ test/intl402/DurationFormat/prototype/format/negative-durationstyle-digital-en.j
default: 'Test262Error: DurationFormat format output using digital style option Expected SameValue(«-1 yr, -2 mths, -3 wks, -3 days, -4:-05:-06», «-1 yr, -2 mths, -3 wks, -3 days, -4:-05:-06.007008009») to be true'
strict mode: 'Test262Error: DurationFormat format output using digital style option Expected SameValue(«-1 yr, -2 mths, -3 wks, -3 days, -4:-05:-06», «-1 yr, -2 mths, -3 wks, -3 days, -4:-05:-06.007008009») to be true'
test/intl402/DurationFormat/prototype/format/precision-exact-mathematical-values.js:
default: 'Test262Error: Duration is {"seconds":10000000,"nanoseconds":1} Expected SameValue(«0:00:10,000,000», «0:00:10,000,000.000000002») to be true'
strict mode: 'Test262Error: Duration is {"seconds":10000000,"nanoseconds":1} Expected SameValue(«0:00:10,000,000», «0:00:10,000,000.000000002») to be true'
default: 'Test262Error: Duration is {"seconds":10000000,"nanoseconds":1} Expected SameValue(«0:00:10,000,000», «0:00:10,000,000.000000001») to be true'
strict mode: 'Test262Error: Duration is {"seconds":10000000,"nanoseconds":1} Expected SameValue(«0:00:10,000,000», «0:00:10,000,000.000000001») to be true'
test/intl402/DurationFormat/prototype/format/style-digital-en.js:
default: 'Test262Error: Assert DurationFormat format output using digital style option Expected SameValue(«1 yr, 2 mths, 3 wks, 3 days, 4:05:06», «1 yr, 2 mths, 3 wks, 3 days, 4:05:06.007008009») to be true'
strict mode: 'Test262Error: Assert DurationFormat format output using digital style option Expected SameValue(«1 yr, 2 mths, 3 wks, 3 days, 4:05:06», «1 yr, 2 mths, 3 wks, 3 days, 4:05:06.007008009») to be true'
Expand All @@ -1036,9 +1036,6 @@ test/intl402/Locale/prototype/minimize/removing-likely-subtags-first-adds-likely
test/intl402/NumberFormat/prototype/format/useGrouping-extended-en-IN.js:
default: 'Test262Error: notation: "compact" Expected SameValue(«1K», «1T») to be true'
strict mode: 'Test262Error: notation: "compact" Expected SameValue(«1K», «1T») to be true'
test/intl402/NumberFormat/prototype/format/value-decimal-string.js:
default: 'Test262Error: Expected SameValue(«1», «1.0000000000000001») to be true'
strict mode: 'Test262Error: Expected SameValue(«1», «1.0000000000000001») to be true'
test/intl402/Temporal/Duration/compare/relativeto-sub-minute-offset.js:
default: 'RangeError: Cannot compare a duration of years, months, or weeks without a relativeTo option'
strict mode: 'RangeError: Cannot compare a duration of years, months, or weeks without a relativeTo option'
Expand Down
73 changes: 73 additions & 0 deletions Source/JavaScriptCore/runtime/IntlNumberFormat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
#include "JSBoundFunction.h"
#include "JSCInlines.h"
#include "ObjectConstructor.h"
#include "ParseInt.h"
#include <wtf/Range.h>
#include <wtf/unicode/icu/ICUHelpers.h>

Expand Down Expand Up @@ -1530,4 +1531,76 @@ JSValue IntlNumberFormat::formatToParts(JSGlobalObject* globalObject, IntlMathem
}
#endif

IntlMathematicalValue IntlMathematicalValue::parseString(JSGlobalObject* globalObject, StringView view)
{
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);

auto trimmed = view.trim([](auto character) {
return isStrWhiteSpace(character);
});

if (!trimmed.length())
return IntlMathematicalValue { 0.0 };

if (trimmed.length() > 2 && trimmed[0] == '0') {
auto character = trimmed[1];
auto remaining = trimmed.substring(2);
int32_t radix = 0;
if (character == 'b' || character == 'B') {
radix = 2;
if (!remaining.containsOnly<isASCIIBinaryDigit>())
return IntlMathematicalValue { PNaN };
} else if (character == 'o' || character == 'O') {
radix = 8;
if (!remaining.containsOnly<isASCIIOctalDigit>())
return IntlMathematicalValue { PNaN };
} else if (character == 'x' || character == 'X') {
radix = 16;
if (!remaining.containsOnly<isASCIIHexDigit>())
return IntlMathematicalValue { PNaN };
}

if (radix) {
double result = parseInt(remaining, radix);
if (result <= maxSafeInteger())
return IntlMathematicalValue { result };

JSValue bigInt = JSBigInt::parseInt(globalObject, vm, remaining, radix, JSBigInt::ErrorParseMode::IgnoreExceptions, JSBigInt::ParseIntSign::Unsigned);
if (!bigInt)
return IntlMathematicalValue { PNaN };

#if USE(BIGINT32)
if (bigInt.isBigInt32())
return IntlMathematicalValue { value.bigInt32AsInt32() };
#endif

auto* heapBigInt = bigInt.asHeapBigInt();
auto string = heapBigInt->toString(globalObject, 10);
RETURN_IF_EXCEPTION(scope, { });

return IntlMathematicalValue {
IntlMathematicalValue::NumberType::Integer,
false,
string.ascii(),
};
}
}

if (trimmed == "Infinity"_s || trimmed == "+Infinity"_s)
return IntlMathematicalValue { std::numeric_limits<double>::infinity() };

if (trimmed == "-Infinity"_s)
return IntlMathematicalValue { -std::numeric_limits<double>::infinity() };

size_t parsedLength = 0;
double result = parseDouble(trimmed, parsedLength);
if (parsedLength != trimmed.length())
return IntlMathematicalValue { PNaN };
if (!std::isfinite(result))
return IntlMathematicalValue { result };

return IntlMathematicalValue { IntlMathematicalValue::NumberType::Integer, trimmed[0] == '-', trimmed.utf8() };
}

} // namespace JSC
2 changes: 2 additions & 0 deletions Source/JavaScriptCore/runtime/IntlNumberFormat.h
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ class IntlMathematicalValue {
{
}

static IntlMathematicalValue parseString(JSGlobalObject*, StringView);

void ensureNonDouble()
{
if (std::holds_alternative<double>(m_value)) {
Expand Down
16 changes: 2 additions & 14 deletions Source/JavaScriptCore/runtime/IntlNumberFormatInlines.h
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ inline IntlNumberFormat* IntlNumberFormat::unwrapForOldFunctions(JSGlobalObject*
return unwrapForLegacyIntlConstructor<IntlNumberFormat>(globalObject, thisValue, globalObject->numberFormatConstructor());
}

// https://tc39.es/proposal-intl-numberformat-v3/out/numberformat/diff.html#sec-tointlmathematicalvalue
// https://tc39.es/ecma402/#sec-tointlmathematicalvalue
inline IntlMathematicalValue toIntlMathematicalValue(JSGlobalObject* globalObject, JSValue value)
{
VM& vm = globalObject->vm();
Expand Down Expand Up @@ -364,19 +364,7 @@ inline IntlMathematicalValue toIntlMathematicalValue(JSGlobalObject* globalObjec
String string = asString(primitive)->value(globalObject);
RETURN_IF_EXCEPTION(scope, { });

JSValue bigInt = JSBigInt::stringToBigInt(globalObject, string);
if (bigInt) {
// If it is -0, we cannot handle it in JSBigInt. Reparse the string as double.
#if USE(BIGINT32)
if (bigInt.isBigInt32() && !value.bigInt32AsInt32())
return IntlMathematicalValue { jsToNumber(string) };
#endif
if (bigInt.isHeapBigInt() && !asHeapBigInt(bigInt)->length())
return IntlMathematicalValue { jsToNumber(string) };
RELEASE_AND_RETURN(scope, bigIntToIntlMathematicalValue(globalObject, bigInt));
}

return IntlMathematicalValue { jsToNumber(string) };
RELEASE_AND_RETURN(scope, IntlMathematicalValue::parseString(globalObject, WTFMove(string)));
}

} // namespace JSC

0 comments on commit c724273

Please sign in to comment.