mirror of
https://github.com/RGBCube/serenity
synced 2025-05-14 09:24:57 +00:00
LibJS: Implement Number.prototype.toExponential
This commit is contained in:
parent
dc984c53d8
commit
534b2be16f
4 changed files with 244 additions and 0 deletions
|
@ -452,6 +452,7 @@ namespace JS {
|
|||
P(timeZone) \
|
||||
P(timeZoneName) \
|
||||
P(toDateString) \
|
||||
P(toExponential) \
|
||||
P(toFixed) \
|
||||
P(toGMTString) \
|
||||
P(toInstant) \
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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]);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue