summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTimothy Flynn <trflynn89@pm.me>2022-01-03 19:48:44 -0500
committerLinus Groh <mail@linusgroh.de>2022-01-04 13:07:42 +0000
commit534b2be16fd8857f28d00ce96151ef49c737509f (patch)
tree42926c092e6a98240baddf6a2ea91db59afec848
parentdc984c53d8b3976bd83eaa817e00025d0f62ad22 (diff)
downloadserenity-534b2be16fd8857f28d00ce96151ef49c737509f.zip
LibJS: Implement Number.prototype.toExponential
-rw-r--r--Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h1
-rw-r--r--Userland/Libraries/LibJS/Runtime/NumberPrototype.cpp152
-rw-r--r--Userland/Libraries/LibJS/Runtime/NumberPrototype.h1
-rw-r--r--Userland/Libraries/LibJS/Tests/builtins/Number/Number.prototype.toExponential.js90
4 files changed, 244 insertions, 0 deletions
diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h
index 30f5e2ddf0..56aca802e0 100644
--- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h
+++ b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h
@@ -452,6 +452,7 @@ namespace JS {
P(timeZone) \
P(timeZoneName) \
P(toDateString) \
+ P(toExponential) \
P(toFixed) \
P(toGMTString) \
P(toInstant) \
diff --git a/Userland/Libraries/LibJS/Runtime/NumberPrototype.cpp b/Userland/Libraries/LibJS/Runtime/NumberPrototype.cpp
index 5ae2cd7209..0c828888af 100644
--- a/Userland/Libraries/LibJS/Runtime/NumberPrototype.cpp
+++ b/Userland/Libraries/LibJS/Runtime/NumberPrototype.cpp
@@ -47,6 +47,36 @@ static String decimal_digits_to_string(double number)
return builder.build().reverse();
}
+static size_t compute_fraction_digits(double number, int exponent)
+{
+ double integral_part = 0;
+ double fraction_part = modf(number, &integral_part);
+
+ auto fraction = String::number(fraction_part);
+ size_t fraction_digits = 0;
+
+ if (integral_part != 0)
+ fraction_digits = exponent;
+
+ if (auto decimal_index = fraction.find('.'); decimal_index.has_value()) {
+ fraction_digits += fraction.length() - *decimal_index - 1;
+
+ if (integral_part == 0) {
+ --fraction_digits;
+
+ for (size_t i = *decimal_index + 1; (i < fraction.length()) && (fraction[i] == '0'); ++i)
+ --fraction_digits;
+ }
+ } else if (integral_part != 0) {
+ auto integral = decimal_digits_to_string(integral_part);
+
+ for (size_t i = integral.length(); (i > 0) && (integral[i - 1] == '0'); --i)
+ --fraction_digits;
+ }
+
+ return fraction_digits;
+}
+
NumberPrototype::NumberPrototype(GlobalObject& global_object)
: NumberObject(0, *global_object.object_prototype())
{
@@ -57,6 +87,7 @@ void NumberPrototype::initialize(GlobalObject& object)
auto& vm = this->vm();
Object::initialize(object);
u8 attr = Attribute::Configurable | Attribute::Writable;
+ define_native_function(vm.names.toExponential, to_exponential, 1, attr);
define_native_function(vm.names.toFixed, to_fixed, 1, attr);
define_native_function(vm.names.toLocaleString, to_locale_string, 0, attr);
define_native_function(vm.names.toPrecision, to_precision, 1, attr);
@@ -89,6 +120,127 @@ static ThrowCompletionOr<Value> this_number_value(GlobalObject& global_object, V
return vm.throw_completion<TypeError>(global_object, ErrorType::NotAnObjectOfType, "Number");
}
+// 21.1.3.2 Number.prototype.toExponential ( fractionDigits ), https://tc39.es/ecma262/#sec-number.prototype.toexponential
+JS_DEFINE_NATIVE_FUNCTION(NumberPrototype::to_exponential)
+{
+ auto fraction_digits_value = vm.argument(0);
+
+ // 1. Let x be ? thisNumberValue(this value).
+ auto number_value = TRY(this_number_value(global_object, vm.this_value(global_object)));
+
+ // 2. Let f be ? ToIntegerOrInfinity(fractionDigits).
+ auto fraction_digits = TRY(fraction_digits_value.to_integer_or_infinity(global_object));
+
+ // 3. Assert: If fractionDigits is undefined, then f is 0.
+ VERIFY(!fraction_digits_value.is_undefined() || (fraction_digits == 0));
+
+ // 4. If x is not finite, return ! Number::toString(x).
+ if (!number_value.is_finite_number())
+ return js_string(vm, MUST(number_value.to_string(global_object)));
+
+ // 5. If f < 0 or f > 100, throw a RangeError exception.
+ if (fraction_digits < 0 || fraction_digits > 100)
+ return vm.throw_completion<RangeError>(global_object, ErrorType::InvalidFractionDigits);
+
+ // 6. Set x to ℝ(x).
+ auto number = number_value.as_double();
+
+ // 7. Let s be the empty String.
+ auto sign = ""sv;
+
+ String number_string;
+ int exponent = 0;
+
+ // 8. If x < 0, then
+ if (number < 0) {
+ // a. Set s to "-".
+ sign = "-"sv;
+
+ // b. Set x to -x.
+ number = -number;
+ }
+
+ // 9. If x = 0, then
+ if (number == 0) {
+ // a. Let m be the String value consisting of f + 1 occurrences of the code unit 0x0030 (DIGIT ZERO).
+ number_string = String::repeated('0', fraction_digits + 1);
+
+ // b. Let e be 0.
+ exponent = 0;
+ }
+ // 10. Else,
+ else {
+ // FIXME: The computations below fall apart for large values of 'f'. A double typically has 52 mantissa bits, which gives us
+ // up to 2^52 before loss of precision. However, the largest value of 'f' may be 100, resulting in numbers on the order
+ // of 10^100, thus we lose precision in these computations.
+
+ // a. If fractionDigits is not undefined, then
+ // i. Let e and n be integers such that 10^f ≤ n < 10^(f+1) and for which n × 10^(e-f) - x is as close to zero as possible.
+ // If there are two such sets of e and n, pick the e and n for which n × 10^(e-f) is larger.
+ // b. Else,
+ // i. Let e, n, and f be integers such that f ≥ 0, 10^f ≤ n < 10^(f+1), 𝔽(n × 10^(e-f)) is 𝔽(x), and f is as small as possible.
+ // Note that the decimal representation of n has f + 1 digits, n is not divisible by 10, and the least significant digit of n is not necessarily uniquely determined by these criteria.
+ exponent = static_cast<int>(floor(log10(number)));
+
+ if (fraction_digits_value.is_undefined())
+ fraction_digits = compute_fraction_digits(number, exponent);
+
+ number = round(number / pow(10, exponent - fraction_digits));
+
+ // c. Let m be the String value consisting of the digits of the decimal representation of n (in order, with no leading zeroes).
+ number_string = decimal_digits_to_string(number);
+ }
+
+ // 11. If f ≠ 0, then
+ if (fraction_digits != 0) {
+ // a. Let a be the first code unit of m.
+ auto first = number_string.substring_view(0, 1);
+
+ // b. Let b be the other f code units of m.
+ auto second = number_string.substring_view(1);
+
+ // c. Set m to the string-concatenation of a, ".", and b.
+ number_string = String::formatted("{}.{}", first, second);
+ }
+
+ char exponent_sign = 0;
+ String exponent_string;
+
+ // 12. If e = 0, then
+ if (exponent == 0) {
+ // a. Let c be "+".
+ exponent_sign = '+';
+
+ // b. Let d be "0".
+ exponent_string = "0"sv;
+ }
+ // 13. Else,
+ else {
+ // a. If e > 0, let c be "+".
+ if (exponent > 0) {
+ exponent_sign = '+';
+ }
+ // b. Else,
+ else {
+ // i. Assert: e < 0.
+ VERIFY(exponent < 0);
+
+ // ii. Let c be "-".
+ exponent_sign = '-';
+
+ // iii. Set e to -e.
+ exponent = -exponent;
+ }
+
+ // c. Let d be the String value consisting of the digits of the decimal representation of e (in order, with no leading zeroes).
+ exponent_string = String::number(exponent);
+ }
+
+ // 14. Set m to the string-concatenation of m, "e", c, and d.
+ // 15. Return the string-concatenation of s and m.
+ return js_string(vm, String::formatted("{}{}e{}{}", sign, number_string, exponent_sign, exponent_string));
+}
+
// 21.1.3.3 Number.prototype.toFixed ( fractionDigits ), https://tc39.es/ecma262/#sec-number.prototype.tofixed
JS_DEFINE_NATIVE_FUNCTION(NumberPrototype::to_fixed)
{
diff --git a/Userland/Libraries/LibJS/Runtime/NumberPrototype.h b/Userland/Libraries/LibJS/Runtime/NumberPrototype.h
index 8baced75ea..2cdf545364 100644
--- a/Userland/Libraries/LibJS/Runtime/NumberPrototype.h
+++ b/Userland/Libraries/LibJS/Runtime/NumberPrototype.h
@@ -18,6 +18,7 @@ public:
virtual void initialize(GlobalObject&) override;
virtual ~NumberPrototype() override;
+ JS_DECLARE_NATIVE_FUNCTION(to_exponential);
JS_DECLARE_NATIVE_FUNCTION(to_fixed);
JS_DECLARE_NATIVE_FUNCTION(to_locale_string);
JS_DECLARE_NATIVE_FUNCTION(to_precision);
diff --git a/Userland/Libraries/LibJS/Tests/builtins/Number/Number.prototype.toExponential.js b/Userland/Libraries/LibJS/Tests/builtins/Number/Number.prototype.toExponential.js
new file mode 100644
index 0000000000..567139d98b
--- /dev/null
+++ b/Userland/Libraries/LibJS/Tests/builtins/Number/Number.prototype.toExponential.js
@@ -0,0 +1,90 @@
+describe("errors", () => {
+ test("must be called with numeric |this|", () => {
+ [true, [], {}, Symbol("foo"), "bar", 1n].forEach(value => {
+ expect(() => {
+ Number.prototype.toExponential.call(value);
+ }).toThrowWithMessage(TypeError, "Not an object of type Number");
+ });
+ });
+
+ test("fraction digits must be coercible to a number", () => {
+ expect(() => {
+ (0).toExponential(Symbol("foo"));
+ }).toThrowWithMessage(TypeError, "Cannot convert symbol to number");
+
+ expect(() => {
+ (0).toExponential(1n);
+ }).toThrowWithMessage(TypeError, "Cannot convert BigInt to number");
+ });
+
+ test("out of range fraction digits", () => {
+ [-Infinity, -1, 101, Infinity].forEach(value => {
+ expect(() => {
+ (0).toExponential(value);
+ }).toThrowWithMessage(
+ RangeError,
+ "Fraction Digits must be an integer no less than 0, and no greater than 100"
+ );
+ });
+ });
+});
+
+describe("correct behavior", () => {
+ test("special values", () => {
+ [
+ [Infinity, 6, "Infinity"],
+ [-Infinity, 7, "-Infinity"],
+ [NaN, 8, "NaN"],
+ [0, 0, "0e+0"],
+ [0, 1, "0.0e+0"],
+ [0, 3, "0.000e+0"],
+ ].forEach(test => {
+ expect(test[0].toExponential(test[1])).toBe(test[2]);
+ });
+ });
+
+ test("zero exponent", () => {
+ [
+ [1, 0, "1e+0"],
+ [5, 1, "5.0e+0"],
+ [9, 3, "9.000e+0"],
+ ].forEach(test => {
+ expect(test[0].toExponential(test[1])).toBe(test[2]);
+ });
+ });
+
+ test("positive exponent", () => {
+ [
+ [12, 0, "1e+1"],
+ [345, 1, "3.5e+2"],
+ [6789, 3, "6.789e+3"],
+ ].forEach(test => {
+ expect(test[0].toExponential(test[1])).toBe(test[2]);
+ });
+ });
+
+ test("negative exponent", () => {
+ [
+ [0.12, 0, "1e-1"],
+ [0.0345, 1, "3.5e-2"],
+ [0.006789, 3, "6.789e-3"],
+ ].forEach(test => {
+ expect(test[0].toExponential(test[1])).toBe(test[2]);
+ });
+ });
+
+ test("undefined precision", () => {
+ [
+ [123.456, "1.23456e+2"],
+ [13, "1.3e+1"],
+ [100, "1e+2"],
+ [345, "3.45e+2"],
+ [6789, "6.789e+3"],
+ [0.13, "1.3e-1"],
+ [0.0345, "3.45e-2"],
+ [0.006789, "6.789e-3"],
+ ].forEach(test => {
+ expect(test[0].toExponential()).toBe(test[1]);
+ });
+ });
+});