ladybird/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp
Andreas Kling 7025dd1fa7 Libraries: Parse JS strings from UTF-16
Thread UTF-16 string input through JSON, script parsing, Date parsing,
Intl option parsing, Temporal parsing, and the helper library boundaries
that feed those parsers. Preserve ASCII fast paths where the source data
is known to be ASCII.
2026-06-22 19:51:25 +02:00

325 lines
12 KiB
C++

/*
* Copyright (c) 2021-2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/NeverDestroyed.h>
#include <LibCrypto/BigInt/SignedBigInteger.h>
#include <LibJS/Runtime/Array.h>
#include <LibJS/Runtime/BigInt.h>
#include <LibJS/Runtime/Intl/NumberFormat.h>
#include <LibJS/Runtime/NativeFunction.h>
#include <LibJS/Runtime/VM.h>
#include <LibJS/Runtime/ValueInlines.h>
#include <LibUnicode/CurrencyCode.h>
#include <math.h>
namespace JS::Intl {
GC_DEFINE_ALLOCATOR(NumberFormatBase);
GC_DEFINE_ALLOCATOR(NumberFormat);
NumberFormatBase::NumberFormatBase(Object& prototype)
: IntlObject(ConstructWithPrototypeTag::Tag, prototype)
{
}
// 16 NumberFormat Objects, https://tc39.es/ecma402/#numberformat-objects
NumberFormat::NumberFormat(Object& prototype)
: NumberFormatBase(prototype)
{
}
void NumberFormat::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
if (m_bound_format)
visitor.visit(m_bound_format);
}
// 16.2.3 Internal slots, https://tc39.es/ecma402/#sec-intl.numberformat-internal-slots
ReadonlySpan<StringView> NumberFormat::relevant_extension_keys() const
{
// The value of the [[RelevantExtensionKeys]] internal slot is « "nu" ».
static constexpr AK::Array keys { "nu"sv };
return keys;
}
// 16.2.3 Internal slots, https://tc39.es/ecma402/#sec-intl.numberformat-internal-slots
ReadonlySpan<ResolutionOptionDescriptor> NumberFormat::resolution_option_descriptors(VM& vm) const
{
// The value of the [[ResolutionOptionDescriptors]] internal slot is « { [[Key]]: "nu", [[Property]]: "numberingSystem" } ».
auto make_descriptors = [&] {
return to_array<ResolutionOptionDescriptor>({
{ .key = "nu"sv, .property = vm.names.numberingSystem },
});
};
static NeverDestroyed<decltype(make_descriptors())> descriptors { make_descriptors() };
return *descriptors;
}
StringView NumberFormatBase::computed_rounding_priority_string() const
{
switch (m_computed_rounding_priority) {
case ComputedRoundingPriority::Auto:
return "auto"sv;
case ComputedRoundingPriority::MorePrecision:
return "morePrecision"sv;
case ComputedRoundingPriority::LessPrecision:
return "lessPrecision"sv;
default:
VERIFY_NOT_REACHED();
}
}
Value NumberFormat::use_grouping_to_value(VM& vm) const
{
switch (m_use_grouping) {
case Unicode::Grouping::Always:
case Unicode::Grouping::Auto:
case Unicode::Grouping::Min2:
return PrimitiveString::create(vm, Unicode::grouping_to_string(m_use_grouping));
case Unicode::Grouping::False:
return Value(false);
default:
VERIFY_NOT_REACHED();
}
}
void NumberFormat::set_use_grouping(StringOrBoolean const& use_grouping)
{
use_grouping.visit(
[this](StringView grouping) {
m_use_grouping = Unicode::grouping_from_string(grouping);
},
[this](bool grouping) {
VERIFY(!grouping);
m_use_grouping = Unicode::Grouping::False;
});
}
Unicode::RoundingOptions NumberFormatBase::rounding_options() const
{
return {
.type = m_rounding_type,
.mode = m_rounding_mode,
.trailing_zero_display = m_trailing_zero_display,
.min_significant_digits = m_min_significant_digits,
.max_significant_digits = m_max_significant_digits,
.min_fraction_digits = m_min_fraction_digits,
.max_fraction_digits = m_max_fraction_digits,
.min_integer_digits = m_min_integer_digits,
.rounding_increment = m_rounding_increment
};
}
Unicode::DisplayOptions NumberFormatBase::display_options() const
{
Unicode::DisplayOptions options;
options.notation = m_notation;
options.compact_display = m_compact_display;
return options;
}
Unicode::DisplayOptions NumberFormat::display_options() const
{
auto options = Base::display_options();
options.style = m_style;
options.sign_display = m_sign_display;
options.grouping = m_use_grouping;
options.currency = m_currency;
options.currency_display = m_currency_display;
options.currency_sign = m_currency_sign;
options.unit = m_unit;
options.unit_display = m_unit_display;
return options;
}
// 16.5.1 CurrencyDigits ( currency ), https://tc39.es/ecma402/#sec-currencydigits
int currency_digits(StringView currency)
{
// 1. If the ISO 4217 currency and funds code list contains currency as an alphabetic code, return the minor
// unit value corresponding to the currency from the list; otherwise, return 2.
if (auto currency_code = Unicode::get_currency_code(currency); currency_code.has_value())
return currency_code->minor_unit.value_or(2);
return 2;
}
// 16.5.4 PartitionNumberPattern ( numberFormat, x ), https://tc39.es/ecma402/#sec-partitionnumberpattern
Vector<Unicode::NumberFormat::Partition> partition_number_pattern(NumberFormat const& number_format, MathematicalValue const& number)
{
return number_format.formatter().format_to_parts(number.to_value());
}
// 16.5.6 FormatNumeric ( numberFormat, x ), https://tc39.es/ecma402/#sec-formatnumber
Utf16String format_numeric(NumberFormat const& number_format, MathematicalValue const& number)
{
// 1. Let parts be ? PartitionNumberPattern(numberFormat, x).
// 2. Let result be the empty String.
// 3. For each Record { [[Type]], [[Value]] } part in parts, do
// a. Set result to the string-concatenation of result and part.[[Value]].
// 4. Return result.
return number_format.formatter().format(number.to_value());
}
// 16.5.7 FormatNumericToParts ( numberFormat, x ), https://tc39.es/ecma402/#sec-formatnumbertoparts
GC::Ref<Array> format_numeric_to_parts(VM& vm, NumberFormat const& number_format, MathematicalValue const& number)
{
auto& realm = *vm.current_realm();
// 1. Let parts be ? PartitionNumberPattern(numberFormat, x).
auto parts = partition_number_pattern(number_format, number);
// 2. Let result be ! ArrayCreate(0).
auto result = MUST(Array::create(realm, 0));
// 3. Let n be 0.
size_t n = 0;
// 4. For each Record { [[Type]], [[Value]] } part in parts, do
for (auto& part : parts) {
// a. Let O be OrdinaryObjectCreate(%Object.prototype%).
auto object = Object::create(realm, realm.intrinsics().object_prototype());
// b. Perform ! CreateDataPropertyOrThrow(O, "type", part.[[Type]]).
MUST(object->create_data_property_or_throw(vm.names.type, PrimitiveString::create(vm, part.type)));
// c. Perform ! CreateDataPropertyOrThrow(O, "value", part.[[Value]]).
MUST(object->create_data_property_or_throw(vm.names.value, PrimitiveString::create(vm, move(part.value))));
// d. Perform ! CreateDataPropertyOrThrow(result, ! ToString(n), O).
MUST(result->create_data_property_or_throw(n, object));
// e. Increment n by 1.
++n;
}
// 5. Return result.
return result;
}
// 16.5.16 ToIntlMathematicalValue ( value ), https://tc39.es/ecma402/#sec-tointlmathematicalvalue
ThrowCompletionOr<MathematicalValue> to_intl_mathematical_value(VM& vm, Value value)
{
// 1. Let primValue be ? ToPrimitive(value, number).
auto primitive_value = TRY(value.to_primitive(vm, Value::PreferredType::Number));
// 2. If Type(primValue) is BigInt, return the mathematical value of primValue.
if (primitive_value.is_bigint())
return Utf16String::from_utf8(MUST(value.as_bigint().big_integer().to_base(10)));
// FIXME: The remaining steps are being refactored into a new Runtime Semantic, StringIntlMV.
// We short-circuit some of these steps to avoid known pitfalls.
// See: https://github.com/tc39/proposal-intl-numberformat-v3/pull/82
if (!primitive_value.is_string()) {
auto number = TRY(primitive_value.to_number(vm));
return number.as_double();
}
// 3. If Type(primValue) is String,
// a. Let str be primValue.
auto string = primitive_value.as_string().utf16_string_view();
// Step 4 handled separately by the FIXME above.
// 5. If the grammar cannot interpret str as an expansion of StringNumericLiteral, return not-a-number.
// 6. Let mv be the MV, a mathematical value, of ? ToNumber(str), as described in 7.1.4.1.1.
auto mathematical_value = TRY(primitive_value.to_number(vm)).as_double();
if (Value(mathematical_value).is_nan())
return MathematicalValue::Symbol::NotANumber;
// 7. If mv is 0 and the first non white space code point in str is -, return negative-zero.
if (mathematical_value == 0.0 && string.trim_ascii_whitespace(TrimMode::Left).starts_with('-'))
return MathematicalValue::Symbol::NegativeZero;
// 8. If mv is 10^10000 and str contains Infinity, return positive-infinity.
if (mathematical_value == pow(10, 10000) && string.contains("Infinity"sv))
return MathematicalValue::Symbol::PositiveInfinity;
// 9. If mv is -10^10000 and str contains Infinity, return negative-infinity.
if (mathematical_value == pow(-10, 10000) && string.contains("Infinity"sv))
return MathematicalValue::Symbol::NegativeInfinity;
// 10. Return mv.
return Utf16String::from_utf16(string);
}
// 16.5.19 PartitionNumberRangePattern ( numberFormat, x, y ), https://tc39.es/ecma402/#sec-partitionnumberrangepattern
ThrowCompletionOr<Vector<Unicode::NumberFormat::Partition>> partition_number_range_pattern(VM& vm, NumberFormat const& number_format, MathematicalValue const& start, MathematicalValue const& end)
{
// 1. If x is NaN or y is NaN, throw a RangeError exception.
if (start.is_nan())
return vm.throw_completion<RangeError>(ErrorType::NumberIsNaN, "start"sv);
if (end.is_nan())
return vm.throw_completion<RangeError>(ErrorType::NumberIsNaN, "end"sv);
return number_format.formatter().format_range_to_parts(start.to_value(), end.to_value());
}
// 16.5.22 FormatNumericRange ( numberFormat, x, y ), https://tc39.es/ecma402/#sec-formatnumericrange
ThrowCompletionOr<Utf16String> format_numeric_range(VM& vm, NumberFormat const& number_format, MathematicalValue const& start, MathematicalValue const& end)
{
// 1. Let parts be ? PartitionNumberRangePattern(numberFormat, x, y).
{
// NOTE: We short-circuit PartitionNumberRangePattern as we do not need individual partitions. But we must still
// perform the NaN sanity checks from its first step.
// 1. If x is NaN or y is NaN, throw a RangeError exception.
if (start.is_nan())
return vm.throw_completion<RangeError>(ErrorType::NumberIsNaN, "start"sv);
if (end.is_nan())
return vm.throw_completion<RangeError>(ErrorType::NumberIsNaN, "end"sv);
}
// 2. Let result be the empty String.
// 3. For each part in parts, do
// a. Set result to the string-concatenation of result and part.[[Value]].
// 4. Return result.
return number_format.formatter().format_range(start.to_value(), end.to_value());
}
// 16.5.23 FormatNumericRangeToParts ( numberFormat, x, y ), https://tc39.es/ecma402/#sec-formatnumericrangetoparts
ThrowCompletionOr<GC::Ref<Array>> format_numeric_range_to_parts(VM& vm, NumberFormat const& number_format, MathematicalValue const& start, MathematicalValue const& end)
{
auto& realm = *vm.current_realm();
// 1. Let parts be ? PartitionNumberRangePattern(numberFormat, x, y).
auto parts = TRY(partition_number_range_pattern(vm, number_format, start, end));
// 2. Let result be ! ArrayCreate(0).
auto result = MUST(Array::create(realm, 0));
// 3. Let n be 0.
size_t n = 0;
// 4. For each Record { [[Type]], [[Value]] } part in parts, do
for (auto& part : parts) {
// a. Let O be OrdinaryObjectCreate(%Object.prototype%).
auto object = Object::create(realm, realm.intrinsics().object_prototype());
// b. Perform ! CreateDataPropertyOrThrow(O, "type", part.[[Type]]).
MUST(object->create_data_property_or_throw(vm.names.type, PrimitiveString::create(vm, part.type)));
// c. Perform ! CreateDataPropertyOrThrow(O, "value", part.[[Value]]).
MUST(object->create_data_property_or_throw(vm.names.value, PrimitiveString::create(vm, move(part.value))));
// d. Perform ! CreateDataPropertyOrThrow(O, "source", part.[[Source]]).
MUST(object->create_data_property_or_throw(vm.names.source, PrimitiveString::create(vm, part.source)));
// e. Perform ! CreateDataPropertyOrThrow(result, ! ToString(n), O).
MUST(result->create_data_property_or_throw(n, object));
// f. Increment n by 1.
++n;
}
// 5. Return result.
return result;
}
}