mirror of
https://github.com/RGBCube/serenity
synced 2025-05-22 00:25:07 +00:00
LibJS: Implement (mostly) spec compliant version of Number.toString()
This commit is contained in:
parent
be9c2feff0
commit
d3524f47a0
4 changed files with 151 additions and 20 deletions
|
@ -364,6 +364,15 @@ int String::replace(const String& needle, const String& replacement, bool all_oc
|
||||||
return positions.size();
|
return positions.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String String::reverse() const
|
||||||
|
{
|
||||||
|
StringBuilder reversed_string;
|
||||||
|
for (size_t i = length(); i-- > 0;) {
|
||||||
|
reversed_string.append(characters()[i]);
|
||||||
|
}
|
||||||
|
return reversed_string.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
String escape_html_entities(const StringView& html)
|
String escape_html_entities(const StringView& html)
|
||||||
{
|
{
|
||||||
StringBuilder builder;
|
StringBuilder builder;
|
||||||
|
|
|
@ -258,6 +258,7 @@ public:
|
||||||
StringView view() const;
|
StringView view() const;
|
||||||
|
|
||||||
int replace(const String& needle, const String& replacement, bool all_occurrences = false);
|
int replace(const String& needle, const String& replacement, bool all_occurrences = false);
|
||||||
|
String reverse() const;
|
||||||
|
|
||||||
template<typename T, typename... Rest>
|
template<typename T, typename... Rest>
|
||||||
bool is_one_of(const T& string, Rest... rest) const
|
bool is_one_of(const T& string, Rest... rest) const
|
||||||
|
|
|
@ -80,6 +80,136 @@ ALWAYS_INLINE bool both_bigint(const Value& lhs, const Value& rhs)
|
||||||
return lhs.is_bigint() && rhs.is_bigint();
|
return lhs.is_bigint() && rhs.is_bigint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static String double_to_string(double d)
|
||||||
|
{
|
||||||
|
// https://tc39.es/ecma262/#sec-numeric-types-number-tostring
|
||||||
|
if (isnan(d))
|
||||||
|
return "NaN";
|
||||||
|
if (d == +0.0 || d == -0.0)
|
||||||
|
return "0";
|
||||||
|
if (d < +0.0) {
|
||||||
|
StringBuilder builder;
|
||||||
|
builder.append('-');
|
||||||
|
builder.append(double_to_string(-d));
|
||||||
|
return builder.to_string();
|
||||||
|
}
|
||||||
|
if (d == INFINITY)
|
||||||
|
return "Infinity";
|
||||||
|
|
||||||
|
StringBuilder number_string_builder;
|
||||||
|
|
||||||
|
size_t start_index = 0;
|
||||||
|
size_t end_index = 0;
|
||||||
|
size_t intpart_end = 0;
|
||||||
|
|
||||||
|
// generate integer part (reversed)
|
||||||
|
double intPart;
|
||||||
|
double frac_part;
|
||||||
|
frac_part = modf(d, &intPart);
|
||||||
|
while (intPart > 0) {
|
||||||
|
number_string_builder.append('0' + (int)fmod(intPart, 10));
|
||||||
|
end_index++;
|
||||||
|
intPart = floor(intPart / 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto reversed_integer_part = number_string_builder.to_string().reverse();
|
||||||
|
number_string_builder.clear();
|
||||||
|
number_string_builder.append(reversed_integer_part);
|
||||||
|
|
||||||
|
intpart_end = end_index;
|
||||||
|
|
||||||
|
int exponent = 0;
|
||||||
|
|
||||||
|
// generate fractional part
|
||||||
|
while (frac_part > 0) {
|
||||||
|
double old_frac_part = frac_part;
|
||||||
|
frac_part *= 10;
|
||||||
|
frac_part = modf(frac_part, &intPart);
|
||||||
|
if (old_frac_part == frac_part)
|
||||||
|
break;
|
||||||
|
number_string_builder.append('0' + (int)intPart);
|
||||||
|
end_index++;
|
||||||
|
exponent--;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto number_string = number_string_builder.to_string();
|
||||||
|
|
||||||
|
// HACK: (sunverwerth) I'm not sure how the ECMAScript spec deals with numbers that
|
||||||
|
// can not be exactly represented in IEE754 so I'm cutting off after the 15th fractional digit.
|
||||||
|
// Otherwise 3.14.toString() would come out as "3.140000000000000124344978758017532527446746826171875"
|
||||||
|
// Chrome and Firefox output the expected "3.14" here
|
||||||
|
if (end_index > intpart_end + 15) {
|
||||||
|
exponent += end_index - intpart_end - 15;
|
||||||
|
end_index = intpart_end + 15;
|
||||||
|
}
|
||||||
|
// HACK end
|
||||||
|
|
||||||
|
// remove leading zeroes
|
||||||
|
while (start_index < end_index && number_string[start_index] == '0') {
|
||||||
|
start_index++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove trailing zeroes
|
||||||
|
while (end_index > 0 && number_string[end_index - 1] == '0') {
|
||||||
|
end_index--;
|
||||||
|
exponent++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (end_index <= start_index)
|
||||||
|
return "0";
|
||||||
|
|
||||||
|
auto digits = number_string.substring_view(start_index, end_index - start_index);
|
||||||
|
|
||||||
|
int number_of_digits = end_index - start_index;
|
||||||
|
|
||||||
|
exponent += number_of_digits;
|
||||||
|
|
||||||
|
StringBuilder builder;
|
||||||
|
|
||||||
|
if (number_of_digits <= exponent && exponent <= 21) {
|
||||||
|
builder.append(digits);
|
||||||
|
builder.append(String::repeated('0', exponent - number_of_digits));
|
||||||
|
return builder.to_string();
|
||||||
|
}
|
||||||
|
if (0 < exponent && exponent <= 21) {
|
||||||
|
builder.append(digits.substring_view(0, exponent));
|
||||||
|
builder.append('.');
|
||||||
|
builder.append(digits.substring_view(exponent));
|
||||||
|
return builder.to_string();
|
||||||
|
}
|
||||||
|
if (-6 < exponent && exponent <= 0) {
|
||||||
|
builder.append("0.");
|
||||||
|
builder.append(String::repeated('0', -exponent));
|
||||||
|
builder.append(digits);
|
||||||
|
return builder.to_string();
|
||||||
|
}
|
||||||
|
if (number_of_digits == 1) {
|
||||||
|
builder.append(digits);
|
||||||
|
builder.append('e');
|
||||||
|
|
||||||
|
if (exponent - 1 > 0)
|
||||||
|
builder.append('+');
|
||||||
|
else
|
||||||
|
builder.append('-');
|
||||||
|
|
||||||
|
builder.append(String::format("%d", abs(exponent - 1)));
|
||||||
|
return builder.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.append(digits[0]);
|
||||||
|
builder.append('.');
|
||||||
|
builder.append(digits.substring_view(1));
|
||||||
|
builder.append('e');
|
||||||
|
|
||||||
|
if (exponent - 1 > 0)
|
||||||
|
builder.append('+');
|
||||||
|
else
|
||||||
|
builder.append('-');
|
||||||
|
|
||||||
|
builder.append(String::format("%d", abs(exponent - 1)));
|
||||||
|
return builder.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
bool Value::is_array() const
|
bool Value::is_array() const
|
||||||
{
|
{
|
||||||
return is_object() && as_object().is_array();
|
return is_object() && as_object().is_array();
|
||||||
|
@ -128,14 +258,7 @@ String Value::to_string_without_side_effects() const
|
||||||
case Type::Boolean:
|
case Type::Boolean:
|
||||||
return m_value.as_bool ? "true" : "false";
|
return m_value.as_bool ? "true" : "false";
|
||||||
case Type::Number:
|
case Type::Number:
|
||||||
if (is_nan())
|
return double_to_string(m_value.as_double);
|
||||||
return "NaN";
|
|
||||||
if (is_infinity())
|
|
||||||
return is_negative_infinity() ? "-Infinity" : "Infinity";
|
|
||||||
if (is_integer())
|
|
||||||
return String::number(as_i32());
|
|
||||||
// FIXME: This should be more sophisticated: don't cut off decimals, don't include trailing zeros
|
|
||||||
return String::formatted("{:.4}", m_value.as_double);
|
|
||||||
case Type::String:
|
case Type::String:
|
||||||
return m_value.as_string->string();
|
return m_value.as_string->string();
|
||||||
case Type::Symbol:
|
case Type::Symbol:
|
||||||
|
@ -173,14 +296,7 @@ String Value::to_string(GlobalObject& global_object, bool legacy_null_to_empty_s
|
||||||
case Type::Boolean:
|
case Type::Boolean:
|
||||||
return m_value.as_bool ? "true" : "false";
|
return m_value.as_bool ? "true" : "false";
|
||||||
case Type::Number:
|
case Type::Number:
|
||||||
if (is_nan())
|
return double_to_string(m_value.as_double);
|
||||||
return "NaN";
|
|
||||||
if (is_infinity())
|
|
||||||
return is_negative_infinity() ? "-Infinity" : "Infinity";
|
|
||||||
if (is_integer())
|
|
||||||
return String::number(as_i32());
|
|
||||||
// FIXME: This should be more sophisticated: don't cut off decimals, don't include trailing zeros
|
|
||||||
return String::formatted("{:.4}", m_value.as_double);
|
|
||||||
case Type::String:
|
case Type::String:
|
||||||
return m_value.as_string->string();
|
return m_value.as_string->string();
|
||||||
case Type::Symbol:
|
case Type::Symbol:
|
||||||
|
|
|
@ -40,10 +40,15 @@ describe("correct behavior", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("numeric keys", () => {
|
test("numeric keys", () => {
|
||||||
expect({0x10:true}).toBe({16:true});
|
const hex = {0x10: "16"};
|
||||||
expect({0b10:true}).toBe({2:true});
|
const oct = {0o10: "8"};
|
||||||
expect({0o10:true}).toBe({8:true});
|
const bin = {0b10: "2"};
|
||||||
expect({.5:true}).toBe({"0.5":true});
|
const float = {.5: "0.5"};
|
||||||
|
|
||||||
|
expect(hex["16"]).toBe("16");
|
||||||
|
expect(oct["8"]).toBe("8");
|
||||||
|
expect(bin["2"]).toBe("2");
|
||||||
|
expect(float["0.5"]).toBe("0.5");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("computed properties", () => {
|
test("computed properties", () => {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue