ladybird/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp
Andreas Kling b6bef6b688 Libraries: Use UTF-16 for JS-visible runtime strings
Produce JS-visible string results as UTF-16 at their source, including
numeric formatting, BigInt and BigFraction formatting, URI encoding,
console formatting, parser errors, regular expression errors, Intl and
Temporal records, LibUnicode locale boundaries, and LibWeb bindings.

Handle fractional radix formatting through the UTF-16 builder view.
2026-06-22 19:51:25 +02:00

326 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<Utf16View> NumberFormat::relevant_extension_keys() const
{
// The value of the [[RelevantExtensionKeys]] internal slot is « "nu" ».
static constexpr AK::Array<Utf16View, 1> 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;
}
Utf16String NumberFormatBase::computed_rounding_priority_string() const
{
switch (m_computed_rounding_priority) {
case ComputedRoundingPriority::Auto:
return "auto"_utf16;
case ComputedRoundingPriority::MorePrecision:
return "morePrecision"_utf16;
case ComputedRoundingPriority::LessPrecision:
return "lessPrecision"_utf16;
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(Utf16View 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.bytes()); 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, move(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 MUST(value.as_bigint().big_integer().to_base_utf16(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, move(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, move(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;
}
}