diff --git a/Userland/Libraries/LibJS/CMakeLists.txt b/Userland/Libraries/LibJS/CMakeLists.txt index 24f077603d6..bb5d1145350 100644 --- a/Userland/Libraries/LibJS/CMakeLists.txt +++ b/Userland/Libraries/LibJS/CMakeLists.txt @@ -114,6 +114,7 @@ set(SOURCES Runtime/Intl/Locale.cpp Runtime/Intl/LocaleConstructor.cpp Runtime/Intl/LocalePrototype.cpp + Runtime/Intl/MathematicalValue.cpp Runtime/Intl/NumberFormat.cpp Runtime/Intl/NumberFormatConstructor.cpp Runtime/Intl/NumberFormatFunction.cpp diff --git a/Userland/Libraries/LibJS/Forward.h b/Userland/Libraries/LibJS/Forward.h index ac18abc83fc..ddafc417761 100644 --- a/Userland/Libraries/LibJS/Forward.h +++ b/Userland/Libraries/LibJS/Forward.h @@ -225,6 +225,8 @@ namespace Intl { JS_ENUMERATE_INTL_OBJECTS #undef __JS_ENUMERATE +class MathematicalValue; + // Not included in JS_ENUMERATE_INTL_OBJECTS due to missing distinct constructor class Segments; class SegmentsPrototype; diff --git a/Userland/Libraries/LibJS/Runtime/Intl/MathematicalValue.cpp b/Userland/Libraries/LibJS/Runtime/Intl/MathematicalValue.cpp new file mode 100644 index 00000000000..5d819a5f742 --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/Intl/MathematicalValue.cpp @@ -0,0 +1,346 @@ +/* + * Copyright (c) 2022, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include + +namespace JS::Intl { + +bool MathematicalValue::is_number() const +{ + return m_value.has(); +} + +double MathematicalValue::as_number() const +{ + VERIFY(is_number()); + return m_value.get(); +} + +bool MathematicalValue::is_bigint() const +{ + return m_value.has(); +} + +Crypto::SignedBigInteger const& MathematicalValue::as_bigint() const +{ + VERIFY(is_bigint()); + return m_value.get(); +} + +bool MathematicalValue::is_mathematical_value() const +{ + return is_number() || is_bigint(); +} + +bool MathematicalValue::is_positive_infinity() const +{ + if (is_mathematical_value()) + return false; + return m_value.get() == Symbol::PositiveInfinity; +} + +bool MathematicalValue::is_negative_infinity() const +{ + if (is_mathematical_value()) + return false; + return m_value.get() == Symbol::NegativeInfinity; +} + +bool MathematicalValue::is_negative_zero() const +{ + if (is_mathematical_value()) + return false; + return m_value.get() == Symbol::NegativeZero; +} + +bool MathematicalValue::is_nan() const +{ + if (is_mathematical_value()) + return false; + return m_value.get() == Symbol::NotANumber; +} + +void MathematicalValue::negate() +{ + m_value.visit( + [](double& value) { + VERIFY(value != 0.0); + value *= -1.0; + }, + [](Crypto::SignedBigInteger& value) { value.negate(); }, + [](auto) { VERIFY_NOT_REACHED(); }); +} + +MathematicalValue MathematicalValue::plus(Checked addition) const +{ + return m_value.visit( + [&](double value) { + return MathematicalValue { value + addition.value() }; + }, + [&](Crypto::SignedBigInteger const& value) { + return MathematicalValue { value.plus(Crypto::SignedBigInteger::create_from(addition.value())) }; + }, + [](auto) -> MathematicalValue { VERIFY_NOT_REACHED(); }); +} + +MathematicalValue MathematicalValue::plus(MathematicalValue const& addition) const +{ + return m_value.visit( + [&](double value) { + return MathematicalValue { value + addition.as_number() }; + }, + [&](Crypto::SignedBigInteger const& value) { + return MathematicalValue { value.plus(addition.as_bigint()) }; + }, + [](auto) -> MathematicalValue { VERIFY_NOT_REACHED(); }); +} + +MathematicalValue MathematicalValue::minus(Checked subtraction) const +{ + return m_value.visit( + [&](double value) { + return MathematicalValue { value - subtraction.value() }; + }, + [&](Crypto::SignedBigInteger const& value) { + return MathematicalValue { value.minus(Crypto::SignedBigInteger::create_from(subtraction.value())) }; + }, + [](auto) -> MathematicalValue { VERIFY_NOT_REACHED(); }); +} + +MathematicalValue MathematicalValue::minus(MathematicalValue const& subtraction) const +{ + return m_value.visit( + [&](double value) { + return MathematicalValue { value - subtraction.as_number() }; + }, + [&](Crypto::SignedBigInteger const& value) { + return MathematicalValue { value.minus(subtraction.as_bigint()) }; + }, + [](auto) -> MathematicalValue { VERIFY_NOT_REACHED(); }); +} + +MathematicalValue MathematicalValue::multiplied_by(Checked multiplier) const +{ + return m_value.visit( + [&](double value) { + return MathematicalValue { value * multiplier.value() }; + }, + [&](Crypto::SignedBigInteger const& value) { + return MathematicalValue { value.multiplied_by(Crypto::SignedBigInteger::create_from(multiplier.value())) }; + }, + [](auto) -> MathematicalValue { VERIFY_NOT_REACHED(); }); +} + +MathematicalValue MathematicalValue::multiplied_by(MathematicalValue const& multiplier) const +{ + return m_value.visit( + [&](double value) { + return MathematicalValue { value * multiplier.as_number() }; + }, + [&](Crypto::SignedBigInteger const& value) { + return MathematicalValue { value.multiplied_by(multiplier.as_bigint()) }; + }, + [](auto) -> MathematicalValue { VERIFY_NOT_REACHED(); }); +} + +MathematicalValue MathematicalValue::divided_by(Checked divisor) const +{ + return m_value.visit( + [&](double value) { + return MathematicalValue { value / divisor.value() }; + }, + [&](Crypto::SignedBigInteger const& value) { + return MathematicalValue { value.divided_by(Crypto::SignedBigInteger::create_from(divisor.value())).quotient }; + }, + [](auto) -> MathematicalValue { VERIFY_NOT_REACHED(); }); +} + +MathematicalValue MathematicalValue::divided_by(MathematicalValue const& divisor) const +{ + return m_value.visit( + [&](double value) { + return MathematicalValue { value / divisor.as_number() }; + }, + [&](Crypto::SignedBigInteger const& value) { + return MathematicalValue { value.divided_by(divisor.as_bigint()).quotient }; + }, + [](auto) -> MathematicalValue { VERIFY_NOT_REACHED(); }); +} + +static Crypto::SignedBigInteger bigint_power(Checked exponent) +{ + VERIFY(exponent >= 0); + + static auto base = Crypto::SignedBigInteger::create_from(10); + auto result = Crypto::SignedBigInteger::create_from(1); + + for (i32 i = 0; i < exponent; ++i) + result = result.multiplied_by(base); + + return result; +} + +MathematicalValue MathematicalValue::multiplied_by_power(Checked exponent) const +{ + return m_value.visit( + [&](double value) { + return MathematicalValue { value * pow(10, exponent.value()) }; + }, + [&](Crypto::SignedBigInteger const& value) { + if (exponent < 0) + return MathematicalValue { value.divided_by(bigint_power(-exponent.value())).quotient }; + return MathematicalValue { value.multiplied_by(bigint_power(exponent)) }; + }, + [](auto) -> MathematicalValue { VERIFY_NOT_REACHED(); }); +} + +MathematicalValue MathematicalValue::divided_by_power(Checked exponent) const +{ + return m_value.visit( + [&](double value) { + if (exponent < 0) + return MathematicalValue { value * pow(10, -exponent.value()) }; + return MathematicalValue { value / pow(10, exponent.value()) }; + }, + [&](Crypto::SignedBigInteger const& value) { + if (exponent < 0) + return MathematicalValue { value.multiplied_by(bigint_power(-exponent.value())) }; + return MathematicalValue { value.divided_by(bigint_power(exponent)).quotient }; + }, + [](auto) -> MathematicalValue { VERIFY_NOT_REACHED(); }); +} + +bool MathematicalValue::modulo_is_zero(Checked mod) const +{ + return m_value.visit( + [&](double value) { + auto result = MathematicalValue { modulo(value, mod.value()) }; + return result.is_equal_to(MathematicalValue { 0.0 }); + }, + [&](Crypto::SignedBigInteger const& value) { + return modulo(value, Crypto::SignedBigInteger::create_from(mod.value())).is_zero(); + }, + [](auto) -> bool { VERIFY_NOT_REACHED(); }); +} + +int MathematicalValue::logarithmic_floor() const +{ + return m_value.visit( + [](double value) { + return static_cast(floor(log10(value))); + }, + [](Crypto::SignedBigInteger const& value) { + // FIXME: Can we do this without string conversion? + return static_cast(value.to_base(10).length() - 1); + }, + [](auto) -> int { VERIFY_NOT_REACHED(); }); +} + +bool MathematicalValue::is_equal_to(MathematicalValue const& other) const +{ + return m_value.visit( + [&](double value) { + static constexpr double epsilon = 5e-14; + return fabs(value - other.as_number()) < epsilon; + }, + [&](Crypto::SignedBigInteger const& value) { + return value == other.as_bigint(); + }, + [](auto) -> bool { VERIFY_NOT_REACHED(); }); +} + +bool MathematicalValue::is_less_than(MathematicalValue const& other) const +{ + return m_value.visit( + [&](double value) { + if (is_equal_to(other)) + return false; + return value < other.as_number(); + }, + [&](Crypto::SignedBigInteger const& value) { + return value < other.as_bigint(); + }, + [](auto) -> bool { VERIFY_NOT_REACHED(); }); +} + +bool MathematicalValue::is_negative() const +{ + return m_value.visit( + [](double value) { return value < 0.0; }, + [](Crypto::SignedBigInteger const& value) { return value.is_negative(); }, + [](Symbol symbol) { return symbol == Symbol::NegativeInfinity; }); +} + +bool MathematicalValue::is_positive() const +{ + return m_value.visit( + [](double value) { return value > 0.0; }, + [](Crypto::SignedBigInteger const& value) { return !value.is_zero() && !value.is_negative(); }, + [](Symbol symbol) { return symbol == Symbol::PositiveInfinity; }); +} + +bool MathematicalValue::is_zero() const +{ + return m_value.visit( + [&](double value) { return value == 0.0; }, + [](Crypto::SignedBigInteger const& value) { return value.is_zero(); }, + [](auto) { return false; }); +} + +String MathematicalValue::to_string() const +{ + return m_value.visit( + [](double value) { return Value(value).to_string_without_side_effects(); }, + [](Crypto::SignedBigInteger const& value) { return value.to_base(10); }, + [](auto) -> String { VERIFY_NOT_REACHED(); }); +} + +Value MathematicalValue::to_value(GlobalObject& global_object) const +{ + auto& vm = global_object.vm(); + + return m_value.visit( + [](double value) { + return Value(value); + }, + [&](Crypto::SignedBigInteger const& value) { + return Value(js_bigint(vm, value)); + }, + [](auto symbol) { + switch (symbol) { + case Symbol::PositiveInfinity: + return js_infinity(); + case Symbol::NegativeInfinity: + return js_negative_infinity(); + case Symbol::NegativeZero: + return Value(-0.0); + case Symbol::NotANumber: + return js_nan(); + } + + VERIFY_NOT_REACHED(); + }); +} + +MathematicalValue::ValueType MathematicalValue::value_from_number(double number) +{ + Value value(number); + + if (value.is_positive_infinity()) + return Symbol::PositiveInfinity; + if (value.is_negative_infinity()) + return Symbol::NegativeInfinity; + if (value.is_negative_zero()) + return Symbol::NegativeZero; + if (value.is_nan()) + return Symbol::NotANumber; + return number; +} + +} diff --git a/Userland/Libraries/LibJS/Runtime/Intl/MathematicalValue.h b/Userland/Libraries/LibJS/Runtime/Intl/MathematicalValue.h new file mode 100644 index 00000000000..92fae9e0d2d --- /dev/null +++ b/Userland/Libraries/LibJS/Runtime/Intl/MathematicalValue.h @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2022, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include + +namespace JS::Intl { + +// https://tc39.es/proposal-intl-numberformat-v3/out/numberformat/proposed.html#intl-mathematical-value +class MathematicalValue { +public: + enum class Symbol { + PositiveInfinity, + NegativeInfinity, + NegativeZero, + NotANumber, + }; + + MathematicalValue() = default; + + explicit MathematicalValue(double value) + : m_value(value_from_number(value)) + { + } + + explicit MathematicalValue(Crypto::SignedBigInteger value) + : m_value(move(value)) + { + } + + explicit MathematicalValue(Symbol symbol) + : m_value(symbol) + { + } + + MathematicalValue(Value value) + : m_value(value.is_number() + ? value_from_number(value.as_double()) + : ValueType(value.as_bigint().big_integer())) + { + } + + bool is_number() const; + double as_number() const; + + bool is_bigint() const; + Crypto::SignedBigInteger const& as_bigint() const; + + bool is_mathematical_value() const; + bool is_positive_infinity() const; + bool is_negative_infinity() const; + bool is_negative_zero() const; + bool is_nan() const; + + void negate(); + + MathematicalValue plus(Checked addition) const; + MathematicalValue plus(MathematicalValue const& addition) const; + + MathematicalValue minus(Checked subtraction) const; + MathematicalValue minus(MathematicalValue const& subtraction) const; + + MathematicalValue multiplied_by(Checked multiplier) const; + MathematicalValue multiplied_by(MathematicalValue const& multiplier) const; + + MathematicalValue divided_by(Checked divisor) const; + MathematicalValue divided_by(MathematicalValue const& divisor) const; + + MathematicalValue multiplied_by_power(Checked exponent) const; + MathematicalValue divided_by_power(Checked exponent) const; + + bool modulo_is_zero(Checked mod) const; + + int logarithmic_floor() const; + + bool is_equal_to(MathematicalValue const&) const; + bool is_less_than(MathematicalValue const&) const; + + bool is_negative() const; + bool is_positive() const; + bool is_zero() const; + + String to_string() const; + Value to_value(GlobalObject&) const; + +private: + using ValueType = Variant; + + static ValueType value_from_number(double number); + + // NOTE: The specific alignment is to avoid an UBSAN error with Clang i686, due to Clang + // disagreeing with UBSAN on the alignment of doubles. See: + // https://github.com/llvm/llvm-project/issues/54845 + // https://github.com/SerenityOS/serenity/issues/13614 + alignas(8) ValueType m_value { 0.0 }; +}; + +}