mirror of
https://github.com/RGBCube/serenity
synced 2025-07-25 14:57:35 +00:00
LibJS: Add Number.prototype.toString
This commit is contained in:
parent
ec3737510d
commit
02305d01ea
6 changed files with 189 additions and 4 deletions
|
@ -45,6 +45,7 @@
|
||||||
M(InstanceOfOperatorBadPrototype, "'prototype' property of %s is not an object") \
|
M(InstanceOfOperatorBadPrototype, "'prototype' property of %s is not an object") \
|
||||||
M(InvalidAssignToConst, "Invalid assignment to const variable") \
|
M(InvalidAssignToConst, "Invalid assignment to const variable") \
|
||||||
M(InvalidLeftHandAssignment, "Invalid left-hand side in assignment") \
|
M(InvalidLeftHandAssignment, "Invalid left-hand side in assignment") \
|
||||||
|
M(InvalidRadix, "Radix must be an integer no less than 2, and no greater than 36") \
|
||||||
M(IsNotA, "%s is not a %s") \
|
M(IsNotA, "%s is not a %s") \
|
||||||
M(IsNotAEvaluatedFrom, "%s is not a %s (evaluated from '%s')") \
|
M(IsNotAEvaluatedFrom, "%s is not a %s (evaluated from '%s')") \
|
||||||
M(IterableNextBadReturn, "iterator.next() returned a non-object value") \
|
M(IterableNextBadReturn, "iterator.next() returned a non-object value") \
|
||||||
|
@ -62,6 +63,7 @@
|
||||||
M(NotASymbol, "%s is not a symbol") \
|
M(NotASymbol, "%s is not a symbol") \
|
||||||
M(NotIterable, "%s is not iterable") \
|
M(NotIterable, "%s is not iterable") \
|
||||||
M(NonExtensibleDefine, "Cannot define property %s on non-extensible object") \
|
M(NonExtensibleDefine, "Cannot define property %s on non-extensible object") \
|
||||||
|
M(NumberIncompatibleThis, "Number.prototype.%s method called with incompatible this target") \
|
||||||
M(ObjectDefinePropertyReturnedFalse, "Object's [[DefineProperty]] method returned false") \
|
M(ObjectDefinePropertyReturnedFalse, "Object's [[DefineProperty]] method returned false") \
|
||||||
M(ObjectSetPrototypeOfReturnedFalse, "Object's [[SetPrototypeOf]] method returned false") \
|
M(ObjectSetPrototypeOfReturnedFalse, "Object's [[SetPrototypeOf]] method returned false") \
|
||||||
M(ObjectSetPrototypeOfTwoArgs, "Object.setPrototypeOf requires at least two arguments") \
|
M(ObjectSetPrototypeOfTwoArgs, "Object.setPrototypeOf requires at least two arguments") \
|
||||||
|
|
|
@ -42,6 +42,8 @@ public:
|
||||||
virtual bool is_number_object() const override { return true; }
|
virtual bool is_number_object() const override { return true; }
|
||||||
virtual Value value_of() const override { return Value(m_value); }
|
virtual Value value_of() const override { return Value(m_value); }
|
||||||
|
|
||||||
|
double number() const { return m_value; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
double m_value { 0 };
|
double m_value { 0 };
|
||||||
};
|
};
|
||||||
|
|
|
@ -25,18 +25,117 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <LibJS/Interpreter.h>
|
#include <LibJS/Interpreter.h>
|
||||||
|
#include <LibJS/Runtime/Error.h>
|
||||||
#include <LibJS/Runtime/GlobalObject.h>
|
#include <LibJS/Runtime/GlobalObject.h>
|
||||||
|
#include <LibJS/Runtime/NumberObject.h>
|
||||||
#include <LibJS/Runtime/NumberPrototype.h>
|
#include <LibJS/Runtime/NumberPrototype.h>
|
||||||
|
|
||||||
namespace JS {
|
namespace JS {
|
||||||
|
|
||||||
|
static const u8 max_precision_for_radix[37] = {
|
||||||
|
0, 0, 52, 32, 26, 22, 20, 18, 17, 16,
|
||||||
|
15, 15, 14, 14, 13, 13, 13, 12, 12, 12,
|
||||||
|
12, 11, 11, 11, 11, 11, 11, 10, 10, 10,
|
||||||
|
10, 10, 10, 10, 10, 10, 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
static char digits[] = "0123456789abcdefghijklmnopqrstuvwxyz";
|
||||||
|
|
||||||
NumberPrototype::NumberPrototype(GlobalObject& global_object)
|
NumberPrototype::NumberPrototype(GlobalObject& global_object)
|
||||||
: NumberObject(0, *global_object.object_prototype())
|
: NumberObject(0, *global_object.object_prototype())
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NumberPrototype::initialize(Interpreter& interpreter, GlobalObject& object)
|
||||||
|
{
|
||||||
|
Object::initialize(interpreter, object);
|
||||||
|
|
||||||
|
define_native_function("toString", to_string, 1, Attribute::Configurable | Attribute::Writable);
|
||||||
|
}
|
||||||
|
|
||||||
NumberPrototype::~NumberPrototype()
|
NumberPrototype::~NumberPrototype()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JS_DEFINE_NATIVE_FUNCTION(NumberPrototype::to_string)
|
||||||
|
{
|
||||||
|
Value number_value;
|
||||||
|
|
||||||
|
auto this_value = interpreter.this_value(global_object);
|
||||||
|
if (this_value.is_number()) {
|
||||||
|
number_value = this_value;
|
||||||
|
} else if (this_value.is_object() && this_value.as_object().is_number_object()) {
|
||||||
|
number_value = static_cast<NumberObject&>(this_value.as_object()).value_of();
|
||||||
|
} else {
|
||||||
|
return interpreter.throw_exception<TypeError>(ErrorType::NumberIncompatibleThis, "toString");
|
||||||
|
}
|
||||||
|
|
||||||
|
int radix;
|
||||||
|
auto argument = interpreter.argument(0);
|
||||||
|
if (argument.is_undefined()) {
|
||||||
|
radix = 10;
|
||||||
|
} else {
|
||||||
|
radix = argument.to_i32(interpreter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interpreter.exception() || radix < 2 || radix > 36)
|
||||||
|
return interpreter.throw_exception<RangeError>(ErrorType::InvalidRadix);
|
||||||
|
|
||||||
|
if (number_value.is_positive_infinity())
|
||||||
|
return js_string(interpreter, "Infinity");
|
||||||
|
if (number_value.is_negative_infinity())
|
||||||
|
return js_string(interpreter, "-Infinity");
|
||||||
|
if (number_value.is_nan())
|
||||||
|
return js_string(interpreter, "NaN");
|
||||||
|
if (number_value.is_positive_zero() || number_value.is_negative_zero())
|
||||||
|
return js_string(interpreter, "0");
|
||||||
|
|
||||||
|
double number = number_value.as_double();
|
||||||
|
bool negative = number < 0;
|
||||||
|
if (negative)
|
||||||
|
number *= -1;
|
||||||
|
|
||||||
|
int int_part = floor(number);
|
||||||
|
double decimal_part = number - int_part;
|
||||||
|
|
||||||
|
Vector<char> backwards_characters;
|
||||||
|
|
||||||
|
if (int_part == 0) {
|
||||||
|
backwards_characters.append('0');
|
||||||
|
} else {
|
||||||
|
while (int_part > 0) {
|
||||||
|
backwards_characters.append(digits[int_part % radix]);
|
||||||
|
int_part /= radix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector<char> characters;
|
||||||
|
if (negative)
|
||||||
|
characters.append('-');
|
||||||
|
|
||||||
|
// Reverse characters;
|
||||||
|
for (ssize_t i = backwards_characters.size() - 1; i >= 0; --i) {
|
||||||
|
characters.append(backwards_characters[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// decimal part
|
||||||
|
if (decimal_part != 0.0) {
|
||||||
|
characters.append('.');
|
||||||
|
|
||||||
|
int precision = max_precision_for_radix[radix];
|
||||||
|
|
||||||
|
for (int i = 0; i < precision; ++i) {
|
||||||
|
decimal_part *= radix;
|
||||||
|
int integral = floor(decimal_part);
|
||||||
|
characters.append(digits[integral]);
|
||||||
|
decimal_part -= integral;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (characters.last() == '0')
|
||||||
|
characters.take_last();
|
||||||
|
}
|
||||||
|
|
||||||
|
return js_string(interpreter, String(characters.data(), characters.size()));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,10 @@ class NumberPrototype final : public NumberObject {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit NumberPrototype(GlobalObject&);
|
explicit NumberPrototype(GlobalObject&);
|
||||||
|
virtual void initialize(Interpreter&, GlobalObject&) override;
|
||||||
virtual ~NumberPrototype() override;
|
virtual ~NumberPrototype() override;
|
||||||
|
|
||||||
|
JS_DECLARE_NATIVE_FUNCTION(to_string);
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,10 +15,6 @@ describe("normal behavior", () => {
|
||||||
expect(["foo", "bar", "baz"].toLocaleString()).toBe("foo,bar,baz");
|
expect(["foo", "bar", "baz"].toLocaleString()).toBe("foo,bar,baz");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("number stringification differs from regular toString, for now", () => {
|
|
||||||
expect([1, 2, 3].toLocaleString()).toBe("[object Number],[object Number],[object Number]");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("null and undefined result in empty strings", () => {
|
test("null and undefined result in empty strings", () => {
|
||||||
expect([null].toLocaleString()).toBe("");
|
expect([null].toLocaleString()).toBe("");
|
||||||
expect([undefined].toLocaleString()).toBe("");
|
expect([undefined].toLocaleString()).toBe("");
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
describe("correct behavior", () => {
|
||||||
|
test("length", () => {
|
||||||
|
expect(Number.prototype.toString).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("basic functionality", () => {
|
||||||
|
[
|
||||||
|
[+0, "0"],
|
||||||
|
[-0, "0"],
|
||||||
|
[Infinity, "Infinity"],
|
||||||
|
[-Infinity, "-Infinity"],
|
||||||
|
[NaN, "NaN"],
|
||||||
|
[12, "12"],
|
||||||
|
[93465, "93465"],
|
||||||
|
[358000, "358000"],
|
||||||
|
].forEach(testCase => {
|
||||||
|
expect(testCase[0].toString()).toBe(testCase[1]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("radix", () => {
|
||||||
|
let number = 7857632;
|
||||||
|
|
||||||
|
[
|
||||||
|
[2, "11101111110010111100000"],
|
||||||
|
[3, "112210012122102"],
|
||||||
|
[4, "131332113200"],
|
||||||
|
[5, "4002421012"],
|
||||||
|
[6, "440225532"],
|
||||||
|
[7, "123534356"],
|
||||||
|
[8, "35762740"],
|
||||||
|
[9, "15705572"],
|
||||||
|
[10, "7857632"],
|
||||||
|
[11, "4487612"],
|
||||||
|
[12, "276b2a8"],
|
||||||
|
[13, "18216b3"],
|
||||||
|
[14, "10877d6"],
|
||||||
|
[15, "a532c2"],
|
||||||
|
[16, "77e5e0"],
|
||||||
|
[17, "59160b"],
|
||||||
|
[18, "42f5h2"],
|
||||||
|
[19, "335b5b"],
|
||||||
|
[20, "29241c"],
|
||||||
|
[21, "1j89fk"],
|
||||||
|
[22, "1bbkh2"],
|
||||||
|
[23, "151ih4"],
|
||||||
|
[24, "ng9h8"],
|
||||||
|
[25, "k2m57"],
|
||||||
|
[26, "h51ig"],
|
||||||
|
[27, "el5hb"],
|
||||||
|
[28, "clqdk"],
|
||||||
|
[29, "b355o"],
|
||||||
|
[30, "9l0l2"],
|
||||||
|
[31, "8fng0"],
|
||||||
|
[32, "7fpf0"],
|
||||||
|
[33, "6klf2"],
|
||||||
|
[34, "5tv8s"],
|
||||||
|
[35, "589dr"],
|
||||||
|
[36, "4oezk"],
|
||||||
|
].forEach(testCase => {
|
||||||
|
expect(number.toString(testCase[0])).toBe(testCase[1]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("decimal radix gets converted to int", () => {
|
||||||
|
expect((30).toString(10.1)).toBe("30");
|
||||||
|
expect((30).toString(10.9)).toBe("30");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("errors", () => {
|
||||||
|
test("must be called with numeric |this|", () => {
|
||||||
|
[true, [], {}, Symbol("foo"), "bar", 1n].forEach(value => {
|
||||||
|
expect(() => Number.prototype.toString.call(value)).toThrow(TypeError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("radix RangeError", () => {
|
||||||
|
[0, 1, 37, 100].forEach(value => {
|
||||||
|
expect(() => (0).toString(value)).toThrow(RangeError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Add table
Add a link
Reference in a new issue