diff --git a/Libraries/LibJS/Runtime/ErrorTypes.h b/Libraries/LibJS/Runtime/ErrorTypes.h index 7be359176b..4d19d24d3e 100644 --- a/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Libraries/LibJS/Runtime/ErrorTypes.h @@ -45,6 +45,7 @@ M(InstanceOfOperatorBadPrototype, "'prototype' property of %s is not an object") \ M(InvalidAssignToConst, "Invalid assignment to const variable") \ 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(IsNotAEvaluatedFrom, "%s is not a %s (evaluated from '%s')") \ M(IterableNextBadReturn, "iterator.next() returned a non-object value") \ @@ -62,6 +63,7 @@ M(NotASymbol, "%s is not a symbol") \ M(NotIterable, "%s is not iterable") \ 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(ObjectSetPrototypeOfReturnedFalse, "Object's [[SetPrototypeOf]] method returned false") \ M(ObjectSetPrototypeOfTwoArgs, "Object.setPrototypeOf requires at least two arguments") \ diff --git a/Libraries/LibJS/Runtime/NumberObject.h b/Libraries/LibJS/Runtime/NumberObject.h index d5574a4de3..ffb394b6c9 100644 --- a/Libraries/LibJS/Runtime/NumberObject.h +++ b/Libraries/LibJS/Runtime/NumberObject.h @@ -42,6 +42,8 @@ public: virtual bool is_number_object() const override { return true; } virtual Value value_of() const override { return Value(m_value); } + double number() const { return m_value; } + private: double m_value { 0 }; }; diff --git a/Libraries/LibJS/Runtime/NumberPrototype.cpp b/Libraries/LibJS/Runtime/NumberPrototype.cpp index 85c874ab04..aebc4b695c 100644 --- a/Libraries/LibJS/Runtime/NumberPrototype.cpp +++ b/Libraries/LibJS/Runtime/NumberPrototype.cpp @@ -25,18 +25,117 @@ */ #include +#include #include +#include #include 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) : 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() { } +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(this_value.as_object()).value_of(); + } else { + return interpreter.throw_exception(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(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 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 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())); +} + } diff --git a/Libraries/LibJS/Runtime/NumberPrototype.h b/Libraries/LibJS/Runtime/NumberPrototype.h index bf571ba75a..81ee707b3e 100644 --- a/Libraries/LibJS/Runtime/NumberPrototype.h +++ b/Libraries/LibJS/Runtime/NumberPrototype.h @@ -35,7 +35,10 @@ class NumberPrototype final : public NumberObject { public: explicit NumberPrototype(GlobalObject&); + virtual void initialize(Interpreter&, GlobalObject&) override; virtual ~NumberPrototype() override; + + JS_DECLARE_NATIVE_FUNCTION(to_string); }; } diff --git a/Libraries/LibJS/Tests/builtins/Array/Array.prototype.toLocaleString.js b/Libraries/LibJS/Tests/builtins/Array/Array.prototype.toLocaleString.js index e951dbbe8f..c566848567 100644 --- a/Libraries/LibJS/Tests/builtins/Array/Array.prototype.toLocaleString.js +++ b/Libraries/LibJS/Tests/builtins/Array/Array.prototype.toLocaleString.js @@ -15,10 +15,6 @@ describe("normal behavior", () => { 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", () => { expect([null].toLocaleString()).toBe(""); expect([undefined].toLocaleString()).toBe(""); diff --git a/Libraries/LibJS/Tests/builtins/Number/Number.prototype.toString.js b/Libraries/LibJS/Tests/builtins/Number/Number.prototype.toString.js new file mode 100644 index 0000000000..c07ca792f6 --- /dev/null +++ b/Libraries/LibJS/Tests/builtins/Number/Number.prototype.toString.js @@ -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); + }); + }); +});