Browse Source

Implement prototype of `NumberFormat` (#3669)

* Implement prototype of `NumberFormat`

* Fix clippy

* Misc fixes

* Apply review
pull/3671/head
José Julián Espina 9 months ago committed by GitHub
parent
commit
cf613c9e65
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      Cargo.lock
  2. 2
      Cargo.toml
  3. 9
      core/engine/Cargo.toml
  4. 2
      core/engine/src/builtins/intl/collator/mod.rs
  5. 36
      core/engine/src/builtins/intl/mod.rs
  6. 851
      core/engine/src/builtins/intl/number_format/mod.rs
  7. 1093
      core/engine/src/builtins/intl/number_format/options.rs
  8. 41
      core/engine/src/builtins/intl/number_format/tests.rs
  9. 525
      core/engine/src/builtins/intl/number_format/utils.rs
  10. 18
      core/engine/src/builtins/intl/plural_rules/mod.rs
  11. 1
      core/engine/src/builtins/mod.rs
  12. 35
      core/engine/src/builtins/options.rs
  13. 59
      core/engine/src/context/intrinsics.rs
  14. 4
      core/engine/src/context/mod.rs
  15. 22
      core/engine/src/object/jsobject.rs
  16. 6
      core/engine/src/object/operations.rs
  17. 11
      core/engine/src/realm.rs
  18. 2
      core/engine/src/string/common.rs
  19. 6
      test262_config.toml

2
Cargo.lock generated

@ -405,6 +405,7 @@ dependencies = [
"icu_casemap",
"icu_collator",
"icu_datetime",
"icu_decimal",
"icu_list",
"icu_locid",
"icu_locid_transform",
@ -439,6 +440,7 @@ dependencies = [
"thin-vec",
"thiserror",
"time 0.3.34",
"tinystr",
"web-time",
"writeable",
"yoke",

2
Cargo.toml

@ -67,6 +67,7 @@ static_assertions = "1.1.0"
textwrap = "0.16.0"
thin-vec = "0.2.13"
time = {version = "0.3.34", no-default-features = true, features = ["local-offset", "large-dates", "wasm-bindgen", "parsing", "formatting", "macros"]}
tinystr = "0.7.5"
# ICU4X
@ -85,6 +86,7 @@ icu_provider_adapters = { version = "~1.4.0", default-features = false }
icu_provider_blob = { version = "~1.4.0", default-features = false }
icu_properties = { version = "~1.4.0", default-features = true }
icu_normalizer = { version = "~1.4.1", default-features = true }
icu_decimal = { version = "~1.4.0", default-features = false }
writeable = "~0.5.4"
yoke = "~0.7.3"
zerofrom = "~0.1.3"

9
core/engine/Cargo.toml

@ -28,11 +28,13 @@ intl = [
"dep:icu_casemap",
"dep:icu_list",
"dep:icu_segmenter",
"dep:icu_decimal",
"dep:writeable",
"dep:sys-locale",
"dep:yoke",
"dep:zerofrom",
"dep:fixed_decimal",
"dep:tinystr",
]
fuzz = ["boa_ast/arbitrary", "boa_interner/arbitrary"]
@ -93,6 +95,7 @@ arrayvec = "0.7.4"
intrusive-collections = "0.9.6"
cfg-if = "1.0.0"
time.workspace = true
hashbrown.workspace = true
# intl deps
boa_icu_provider = {workspace = true, features = ["std"], optional = true }
@ -107,11 +110,13 @@ icu_plurals = { workspace = true, default-features = false, features = ["serde",
icu_list = { workspace = true, default-features = false, features = ["serde"], optional = true }
icu_casemap = { workspace = true, default-features = false, features = ["serde"], optional = true}
icu_segmenter = { workspace = true, default-features = false, features = ["auto", "serde"], optional = true }
icu_decimal = { workspace = true, default-features = false, features = ["serde"], optional = true }
writeable = { workspace = true, optional = true }
yoke = { workspace = true, optional = true }
zerofrom = { workspace = true, optional = true }
fixed_decimal = { workspace = true, features = ["ryu", "experimental"], optional = true}
hashbrown.workspace = true
fixed_decimal = { workspace = true, features = ["ryu", "experimental"], optional = true }
tinystr = { workspace = true, optional = true }
[target.'cfg(all(target_family = "wasm", not(any(target_os = "emscripten", target_os = "wasi"))))'.dependencies]
web-time = { version = "1.0.0", optional = true }

2
core/engine/src/builtins/intl/collator/mod.rs

@ -69,7 +69,7 @@ impl Collator {
}
#[derive(Debug, Clone)]
pub(in crate::builtins::intl) struct CollatorLocaleOptions {
pub(super) struct CollatorLocaleOptions {
collation: Option<Value>,
numeric: Option<bool>,
case_first: Option<CaseFirst>,

36
core/engine/src/builtins/intl/mod.rs

@ -22,9 +22,10 @@ use crate::{
realm::Realm,
string::common::StaticJsStrings,
symbol::JsSymbol,
Context, JsArgs, JsResult, JsString, JsValue,
Context, JsArgs, JsData, JsResult, JsString, JsValue,
};
use boa_gc::{Finalize, Trace};
use boa_profiler::Profiler;
use icu_provider::KeyedDataMarker;
@ -38,14 +39,30 @@ pub(crate) mod segmenter;
pub(crate) use self::{
collator::Collator, date_time_format::DateTimeFormat, list_format::ListFormat, locale::Locale,
plural_rules::PluralRules, segmenter::Segmenter,
number_format::NumberFormat, plural_rules::PluralRules, segmenter::Segmenter,
};
mod options;
/// JavaScript `Intl` object.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct Intl;
#[derive(Debug, Clone, Trace, Finalize, JsData)]
#[boa_gc(unsafe_empty_trace)]
pub struct Intl {
fallback_symbol: JsSymbol,
}
impl Intl {
/// Gets this realm's `Intl` object's `[[FallbackSymbol]]` slot.
#[must_use]
pub fn fallback_symbol(&self) -> JsSymbol {
self.fallback_symbol.clone()
}
pub(crate) fn new() -> Option<Self> {
let fallback_symbol = JsSymbol::new(Some(js_string!("IntlLegacyConstructedSymbol")))?;
Some(Self { fallback_symbol })
}
}
impl IntrinsicObject for Intl {
fn init(realm: &Realm) {
@ -99,6 +116,15 @@ impl IntrinsicObject for Intl {
.constructor(),
DateTimeFormat::ATTRIBUTE,
)
.static_property(
NumberFormat::NAME,
realm
.intrinsics()
.constructors()
.number_format()
.constructor(),
NumberFormat::ATTRIBUTE,
)
.static_method(
Self::get_canonical_locales,
js_string!("getCanonicalLocales"),
@ -108,7 +134,7 @@ impl IntrinsicObject for Intl {
}
fn get(intrinsics: &Intrinsics) -> JsObject {
intrinsics.objects().intl()
intrinsics.objects().intl().upcast()
}
}

851
core/engine/src/builtins/intl/number_format/mod.rs

@ -1,4 +1,851 @@
use std::borrow::Cow;
use boa_gc::{Finalize, Trace};
use boa_macros::utf16;
use boa_profiler::Profiler;
use fixed_decimal::{FixedDecimal, FloatPrecision, SignDisplay};
use icu_decimal::{
options::{FixedDecimalFormatterOptions, GroupingStrategy},
provider::DecimalSymbolsV1Marker,
FixedDecimalFormatter, FormattedFixedDecimal,
};
mod options;
mod utils;
use icu_locid::{
extensions::unicode::{key, Value},
Locale,
};
use num_bigint::BigInt;
use num_traits::Num;
pub(crate) use options::*;
pub(crate) use utils::*;
use crate::{
builtins::{
builder::BuiltInBuilder, options::get_option, string::is_trimmable_whitespace,
BuiltInConstructor, BuiltInObject, IntrinsicObject,
},
context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors},
js_string,
object::{
internal_methods::get_prototype_from_constructor, FunctionObjectBuilder, JsFunction,
ObjectInitializer,
},
property::{Attribute, PropertyDescriptor},
realm::Realm,
string::common::StaticJsStrings,
value::PreferredType,
Context, JsArgs, JsData, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue,
NativeFunction,
};
use super::{
locale::{canonicalize_locale_list, resolve_locale, supported_locales, validate_extension},
options::{coerce_options_to_object, IntlOptions},
Service,
};
#[cfg(test)]
mod tests;
#[derive(Debug, Trace, Finalize, JsData)]
// Safety: `NumberFormat` only contains non-traceable types.
#[boa_gc(unsafe_empty_trace)]
pub(crate) struct NumberFormat {
locale: Locale,
formatter: FixedDecimalFormatter,
numbering_system: Option<Value>,
unit_options: UnitFormatOptions,
digit_options: DigitFormatOptions,
notation: Notation,
use_grouping: GroupingStrategy,
sign_display: SignDisplay,
bound_format: Option<JsFunction>,
}
impl NumberFormat {
/// [`FormatNumeric ( numberFormat, x )`][full] and [`FormatNumericToParts ( numberFormat, x )`][parts].
///
/// The returned struct implements `Writable`, allowing to either write the number as a full
/// string or by parts.
///
/// [full]: https://tc39.es/ecma402/#sec-formatnumber
/// [parts]: https://tc39.es/ecma402/#sec-formatnumbertoparts
fn format<'a>(&'a self, value: &'a mut FixedDecimal) -> FormattedFixedDecimal<'a> {
// TODO: Missing support from ICU4X for Percent/Currency/Unit formatting.
// TODO: Missing support from ICU4X for Scientific/Engineering/Compact notation.
self.digit_options.format_fixed_decimal(value);
value.apply_sign_display(self.sign_display);
self.formatter.format(value)
}
}
#[derive(Debug, Clone)]
pub(super) struct NumberFormatLocaleOptions {
numbering_system: Option<Value>,
}
impl Service for NumberFormat {
type LangMarker = DecimalSymbolsV1Marker;
type LocaleOptions = NumberFormatLocaleOptions;
fn resolve(
locale: &mut Locale,
options: &mut Self::LocaleOptions,
provider: &crate::context::icu::IntlProvider,
) {
let numbering_system = options
.numbering_system
.take()
.filter(|nu| {
validate_extension::<Self::LangMarker>(locale.id.clone(), key!("nu"), nu, provider)
})
.or_else(|| {
locale
.extensions
.unicode
.keywords
.get(&key!("nu"))
.cloned()
.filter(|nu| {
validate_extension::<Self::LangMarker>(
locale.id.clone(),
key!("nu"),
nu,
provider,
)
})
});
locale.extensions.unicode.clear();
if let Some(nu) = numbering_system.clone() {
locale.extensions.unicode.keywords.set(key!("nu"), nu);
}
options.numbering_system = numbering_system;
}
}
impl IntrinsicObject for NumberFormat {
fn init(realm: &Realm) {
let _timer = Profiler::global().start_event(std::any::type_name::<Self>(), "init");
let get_format = BuiltInBuilder::callable(realm, Self::get_format)
.name(js_string!("get format"))
.build();
BuiltInBuilder::from_standard_constructor::<Self>(realm)
.static_method(
Self::supported_locales_of,
js_string!("supportedLocalesOf"),
1,
)
.property(
JsSymbol::to_string_tag(),
js_string!("Intl.NumberFormat"),
Attribute::CONFIGURABLE,
)
.accessor(
js_string!("format"),
Some(get_format),
None,
Attribute::CONFIGURABLE,
)
.method(Self::resolved_options, js_string!("resolvedOptions"), 0)
.build();
}
fn get(intrinsics: &Intrinsics) -> JsObject {
Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor()
}
}
impl BuiltInObject for NumberFormat {
const NAME: JsString = StaticJsStrings::NUMBER_FORMAT;
}
impl BuiltInConstructor for NumberFormat {
const LENGTH: usize = 0;
const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor =
StandardConstructors::number_format;
/// [`Intl.NumberFormat ( [ locales [ , options ] ] )`][spec].
///
/// [spec]: https://tc39.es/ecma402/#sec-intl.numberformat
fn constructor(
new_target: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let locales = args.get_or_undefined(0);
let options = args.get_or_undefined(1);
// 1. If NewTarget is undefined, let newTarget be the active function object, else let newTarget be NewTarget.
let new_target_inner = &if new_target.is_undefined() {
context
.active_function_object()
.unwrap_or_else(|| {
context
.intrinsics()
.constructors()
.number_format()
.constructor()
})
.into()
} else {
new_target.clone()
};
// 2. Let numberFormat be ? OrdinaryCreateFromConstructor(newTarget, "%Intl.NumberFormat.prototype%", « [[InitializedNumberFormat]], [[Locale]], [[DataLocale]], [[NumberingSystem]], [[Style]], [[Unit]], [[UnitDisplay]], [[Currency]], [[CurrencyDisplay]], [[CurrencySign]], [[MinimumIntegerDigits]], [[MinimumFractionDigits]], [[MaximumFractionDigits]], [[MinimumSignificantDigits]], [[MaximumSignificantDigits]], [[RoundingType]], [[Notation]], [[CompactDisplay]], [[UseGrouping]], [[SignDisplay]], [[RoundingIncrement]], [[RoundingMode]], [[ComputedRoundingPriority]], [[TrailingZeroDisplay]], [[BoundFormat]] »).
let prototype = get_prototype_from_constructor(
new_target_inner,
StandardConstructors::number_format,
context,
)?;
// 3. Perform ? InitializeNumberFormat(numberFormat, locales, options).
// `InitializeNumberFormat ( numberFormat, locales, options )`
// https://tc39.es/ecma402/#sec-initializenumberformat
// 1. Let requestedLocales be ? CanonicalizeLocaleList(locales).
let requested_locales = canonicalize_locale_list(locales, context)?;
// 2. Set options to ? CoerceOptionsToObject(options).
let options = coerce_options_to_object(options, context)?;
// 3. Let opt be a new Record.
// 4. Let matcher be ? GetOption(options, "localeMatcher", string, « "lookup", "best fit" », "best fit").
// 5. Set opt.[[localeMatcher]] to matcher.
let matcher = get_option(&options, utf16!("localeMatcher"), context)?.unwrap_or_default();
// 6. Let numberingSystem be ? GetOption(options, "numberingSystem", string, empty, undefined).
// 7. If numberingSystem is not undefined, then
// a. If numberingSystem cannot be matched by the type Unicode locale nonterminal, throw a RangeError exception.
// 8. Set opt.[[nu]] to numberingSystem.
let numbering_system = get_option(&options, utf16!("numberingSystem"), context)?;
let mut intl_options = IntlOptions {
matcher,
service_options: NumberFormatLocaleOptions { numbering_system },
};
// 9. Let localeData be %Intl.NumberFormat%.[[LocaleData]].
// 10. Let r be ResolveLocale(%Intl.NumberFormat%.[[AvailableLocales]], requestedLocales, opt, %Intl.NumberFormat%.[[RelevantExtensionKeys]], localeData).
let locale = resolve_locale::<Self>(
&requested_locales,
&mut intl_options,
context.intl_provider(),
);
// 11. Set numberFormat.[[Locale]] to r.[[locale]].
// 12. Set numberFormat.[[DataLocale]] to r.[[dataLocale]].
// 13. Set numberFormat.[[NumberingSystem]] to r.[[nu]].
// 14. Perform ? SetNumberFormatUnitOptions(numberFormat, options).
let unit_options = UnitFormatOptions::from_options(&options, context)?;
// 15. Let style be numberFormat.[[Style]].
// 16. If style is "currency", then
let (min_fractional, max_fractional) = if unit_options.style() == Style::Currency {
// TODO: Missing support from ICU4X
// a. Let currency be numberFormat.[[Currency]].
// b. Let cDigits be CurrencyDigits(currency).
// c. Let mnfdDefault be cDigits.
// d. Let mxfdDefault be cDigits.
return Err(JsNativeError::typ().with_message("unimplemented").into());
} else {
// 17. Else,
(
// a. Let mnfdDefault be 0.
0,
// b. If style is "percent", then
if unit_options.style() == Style::Percent {
// i. Let mxfdDefault be 0.
0
} else {
// c. Else,
// i. Let mxfdDefault be 3.
3
},
)
};
// 18. Let notation be ? GetOption(options, "notation", string, « "standard", "scientific", "engineering", "compact" », "standard").
// 19. Set numberFormat.[[Notation]] to notation.
let notation = get_option(&options, utf16!("notation"), context)?.unwrap_or_default();
// 20. Perform ? SetNumberFormatDigitOptions(numberFormat, options, mnfdDefault, mxfdDefault, notation).
let digit_options = DigitFormatOptions::from_options(
&options,
min_fractional,
max_fractional,
notation,
context,
)?;
// 21. Let compactDisplay be ? GetOption(options, "compactDisplay", string, « "short", "long" », "short").
let compact_display =
get_option(&options, utf16!("compactDisplay"), context)?.unwrap_or_default();
// 22. Let defaultUseGrouping be "auto".
let mut default_use_grouping = GroupingStrategy::Auto;
let notation = match notation {
NotationKind::Standard => Notation::Standard,
NotationKind::Scientific => Notation::Scientific,
NotationKind::Engineering => Notation::Engineering,
// 23. If notation is "compact", then
NotationKind::Compact => {
// b. Set defaultUseGrouping to "min2".
default_use_grouping = GroupingStrategy::Min2;
// a. Set numberFormat.[[CompactDisplay]] to compactDisplay.
Notation::Compact {
display: compact_display,
}
}
};
// 24. NOTE: For historical reasons, the strings "true" and "false" are accepted and replaced with the default value.
// 25. Let useGrouping be ? GetBooleanOrStringNumberFormatOption(options, "useGrouping",
// « "min2", "auto", "always", "true", "false" », defaultUseGrouping).
// 26. If useGrouping is "true" or useGrouping is "false", set useGrouping to defaultUseGrouping.
// 27. If useGrouping is true, set useGrouping to "always".
// 28. Set numberFormat.[[UseGrouping]] to useGrouping.
// useGrouping requires special handling because of the "true" and "false" exceptions.
// We could also modify the `OptionType` interface but it complicates it a lot just for
// a single exception.
let use_grouping = 'block: {
// GetBooleanOrStringNumberFormatOption ( options, property, stringValues, fallback )
// <https://tc39.es/ecma402/#sec-getbooleanorstringnumberformatoption>
// 1. Let value be ? Get(options, property).
let value = options.get(utf16!("useGrouping"), context)?;
// 2. If value is undefined, return fallback.
if value.is_undefined() {
break 'block default_use_grouping;
}
// 3. If value is true, return true.
if let &JsValue::Boolean(true) = &value {
break 'block GroupingStrategy::Always;
}
// 4. If ToBoolean(value) is false, return false.
if !value.to_boolean() {
break 'block GroupingStrategy::Never;
}
// 5. Set value to ? ToString(value).
// 6. If stringValues does not contain value, throw a RangeError exception.
// 7. Return value.
match value.to_string(context)?.to_std_string_escaped().as_str() {
"min2" => GroupingStrategy::Min2,
"auto" => GroupingStrategy::Auto,
"always" => GroupingStrategy::Always,
// special handling for historical reasons
"true" | "false" => default_use_grouping,
_ => {
return Err(JsNativeError::range()
.with_message(
"expected one of `min2`, `auto`, `always`, `true`, or `false`",
)
.into())
}
}
};
// 29. Let signDisplay be ? GetOption(options, "signDisplay", string, « "auto", "never", "always", "exceptZero", "negative" », "auto").
// 30. Set numberFormat.[[SignDisplay]] to signDisplay.
let sign_display =
get_option(&options, utf16!("signDisplay"), context)?.unwrap_or(SignDisplay::Auto);
let formatter = FixedDecimalFormatter::try_new_unstable(
context.intl_provider(),
&locale.clone().into(),
{
let mut options = FixedDecimalFormatterOptions::default();
options.grouping_strategy = use_grouping;
options
},
)
.map_err(|err| JsNativeError::typ().with_message(err.to_string()))?;
let number_format = JsObject::from_proto_and_data_with_shared_shape(
context.root_shape(),
prototype,
NumberFormat {
locale,
numbering_system: intl_options.service_options.numbering_system,
formatter,
unit_options,
digit_options,
notation,
use_grouping,
sign_display,
bound_format: None,
},
);
// 31. Return unused.
// 4. If the implementation supports the normative optional constructor mode of 4.3 Note 1, then
// a. Let this be the this value.
// b. Return ? ChainNumberFormat(numberFormat, NewTarget, this).
// ChainNumberFormat ( numberFormat, newTarget, this )
// <https://tc39.es/ecma402/#sec-chainnumberformat>
let this = context.vm.frame().this(&context.vm);
let Some(this_obj) = this.as_object() else {
return Ok(number_format.into());
};
let constructor = context
.intrinsics()
.constructors()
.number_format()
.constructor();
// 1. If newTarget is undefined and ? OrdinaryHasInstance(%Intl.NumberFormat%, this) is true, then
if new_target.is_undefined()
&& JsValue::ordinary_has_instance(&constructor.into(), &this, context)?
{
let fallback_symbol = context
.intrinsics()
.objects()
.intl()
.borrow()
.data
.fallback_symbol();
// a. Perform ? DefinePropertyOrThrow(this, %Intl%.[[FallbackSymbol]], PropertyDescriptor{ [[Value]]: numberFormat, [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: false }).
this_obj.define_property_or_throw(
fallback_symbol,
PropertyDescriptor::builder()
.value(number_format)
.writable(false)
.enumerable(false)
.configurable(false),
context,
)?;
// b. Return this.
Ok(this)
} else {
// 2. Return numberFormat.
Ok(number_format.into())
}
}
}
impl NumberFormat {
/// [`Intl.NumberFormat.supportedLocalesOf ( locales [ , options ] )`][spec].
///
/// Returns an array containing those of the provided locales that are supported in number format
/// without having to fall back to the runtime's default locale.
///
/// More information:
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma402/#sec-intl.numberformat.supportedlocalesof
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/supportedLocalesOf
fn supported_locales_of(
_: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
let locales = args.get_or_undefined(0);
let options = args.get_or_undefined(1);
// 1. Let availableLocales be %Intl.NumberFormat%.[[AvailableLocales]].
// 2. Let requestedLocales be ? CanonicalizeLocaleList(locales).
let requested_locales = canonicalize_locale_list(locales, context)?;
// 3. Return ? SupportedLocales(availableLocales, requestedLocales, options).
supported_locales::<<Self as Service>::LangMarker>(&requested_locales, options, context)
.map(JsValue::from)
}
/// [`get Intl.NumberFormat.prototype.format`][spec].
///
/// [spec]: https://tc39.es/ecma402/#sec-intl.numberformat.prototype.format
fn get_format(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
// 1. Let nf be the this value.
// 2. If the implementation supports the normative optional constructor mode of 4.3 Note 1, then
// a. Set nf to ? UnwrapNumberFormat(nf).
// 3. Perform ? RequireInternalSlot(nf, [[InitializedNumberFormat]]).
let nf = unwrap_number_format(this, context)?;
let nf_clone = nf.clone();
let mut nf = nf.borrow_mut();
let bound_format = if let Some(f) = nf.data.bound_format.clone() {
f
} else {
// 4. If nf.[[BoundFormat]] is undefined, then
// a. Let F be a new built-in function object as defined in Number Format Functions (15.5.2).
// b. Set F.[[NumberFormat]] to nf.
// c. Set nf.[[BoundFormat]] to F.
let bound_format = FunctionObjectBuilder::new(
context.realm(),
// Number Format Functions
// <https://tc39.es/ecma402/#sec-number-format-functions>
NativeFunction::from_copy_closure_with_captures(
|_, args, nf, context| {
// 1. Let nf be F.[[NumberFormat]].
// 2. Assert: Type(nf) is Object and nf has an [[InitializedNumberFormat]] internal slot.
// 3. If value is not provided, let value be undefined.
let value = args.get_or_undefined(0);
// 4. Let x be ? ToIntlMathematicalValue(value).
let mut x = to_intl_mathematical_value(value, context)?;
// 5. Return FormatNumeric(nf, x).
Ok(js_string!(nf.borrow().data.format(&mut x).to_string()).into())
},
nf_clone,
),
)
.length(2)
.build();
nf.data.bound_format = Some(bound_format.clone());
bound_format
};
// 5. Return nf.[[BoundFormat]].
Ok(bound_format.into())
}
/// [`Intl.NumberFormat.prototype.resolvedOptions ( )`][spec].
///
/// Returns a new object with properties reflecting the locale and options computed during the
/// construction of the current `Intl.NumberFormat` object.
///
/// More information:
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma402/#sec-intl.numberformat.prototype.resolvedoptions
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/resolvedOptions
fn resolved_options(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
// This function provides access to the locale and options computed during initialization of the object.
// 1. Let nf be the this value.
// 2. If the implementation supports the normative optional constructor mode of 4.3 Note 1, then
// a. Set nf to ? UnwrapNumberFormat(nf).
// 3. Perform ? RequireInternalSlot(nf, [[InitializedNumberFormat]]).
let nf = unwrap_number_format(this, context)?;
let nf = nf.borrow();
let nf = &nf.data;
// 4. Let options be OrdinaryObjectCreate(%Object.prototype%).
// 5. For each row of Table 12, except the header row, in table order, do
// a. Let p be the Property value of the current row.
// b. Let v be the value of nf's internal slot whose name is the Internal Slot value of the current row.
// c. If v is not undefined, then
// i. If there is a Conversion value in the current row, then
// 1. Assert: The Conversion value of the current row is number.
// 2. Set v to 𝔽(v).
// ii. Perform ! CreateDataPropertyOrThrow(options, p, v).
let mut options = ObjectInitializer::new(context);
options.property(
js_string!("locale"),
js_string!(nf.locale.to_string()),
Attribute::all(),
);
if let Some(nu) = &nf.numbering_system {
options.property(
js_string!("numberingSystem"),
js_string!(nu.to_string()),
Attribute::all(),
);
}
options.property(
js_string!("style"),
nf.unit_options.style().to_js_string(),
Attribute::all(),
);
match &nf.unit_options {
UnitFormatOptions::Currency {
currency,
display,
sign,
} => {
options.property(
js_string!("currency"),
currency.to_js_string(),
Attribute::all(),
);
options.property(
js_string!("currencyDisplay"),
display.to_js_string(),
Attribute::all(),
);
options.property(
js_string!("currencySign"),
sign.to_js_string(),
Attribute::all(),
);
}
UnitFormatOptions::Unit { unit, display } => {
options.property(js_string!("unit"), unit.to_js_string(), Attribute::all());
options.property(
js_string!("unitDisplay"),
display.to_js_string(),
Attribute::all(),
);
}
UnitFormatOptions::Decimal | UnitFormatOptions::Percent => {}
}
options.property(
js_string!("minimumIntegerDigits"),
nf.digit_options.minimum_integer_digits,
Attribute::all(),
);
if let Some(Extrema { minimum, maximum }) = nf.digit_options.rounding_type.fraction_digits()
{
options
.property(
js_string!("minimumFractionDigits"),
minimum,
Attribute::all(),
)
.property(
js_string!("maximumFractionDigits"),
maximum,
Attribute::all(),
);
}
if let Some(Extrema { minimum, maximum }) =
nf.digit_options.rounding_type.significant_digits()
{
options
.property(
js_string!("minimumSignificantDigits"),
minimum,
Attribute::all(),
)
.property(
js_string!("maximumSignificantDigits"),
maximum,
Attribute::all(),
);
}
let use_grouping = match nf.use_grouping {
GroupingStrategy::Auto => js_string!("auto").into(),
GroupingStrategy::Never => JsValue::from(false),
GroupingStrategy::Always => js_string!("always").into(),
GroupingStrategy::Min2 => js_string!("min2").into(),
_ => {
return Err(JsNativeError::typ()
.with_message("unsupported useGrouping value")
.into())
}
};
options
.property(js_string!("useGrouping"), use_grouping, Attribute::all())
.property(
js_string!("notation"),
nf.notation.kind().to_js_string(),
Attribute::all(),
);
if let Notation::Compact { display } = nf.notation {
options.property(
js_string!("compactDisplay"),
display.to_js_string(),
Attribute::all(),
);
}
let sign_display = match nf.sign_display {
SignDisplay::Auto => js_string!("auto"),
SignDisplay::Never => js_string!("never"),
SignDisplay::Always => js_string!("always"),
SignDisplay::ExceptZero => js_string!("exceptZero"),
SignDisplay::Negative => js_string!("negative"),
_ => {
return Err(JsNativeError::typ()
.with_message("unsupported signDisplay value")
.into())
}
};
options
.property(js_string!("signDisplay"), sign_display, Attribute::all())
.property(
js_string!("roundingIncrement"),
nf.digit_options.rounding_increment.to_u16(),
Attribute::all(),
)
.property(
js_string!("roundingPriority"),
nf.digit_options.rounding_priority.to_js_string(),
Attribute::all(),
)
.property(
js_string!("trailingZeroDisplay"),
nf.digit_options.trailing_zero_display.to_js_string(),
Attribute::all(),
);
// 6. Return options.
Ok(options.build().into())
}
}
/// Abstract operation [`UnwrapNumberFormat ( nf )`][spec].
///
/// This also checks that the returned object is a `NumberFormat`, which skips the
/// call to `RequireInternalSlot`.
///
/// [spec]: https://tc39.es/ecma402/#sec-unwrapnumberformat
fn unwrap_number_format(nf: &JsValue, context: &mut Context) -> JsResult<JsObject<NumberFormat>> {
// 1. If Type(nf) is not Object, throw a TypeError exception.
let nf_o = nf.as_object().ok_or_else(|| {
JsNativeError::typ().with_message("value was not an `Intl.NumberFormat` object")
})?;
if let Ok(nf) = nf_o.clone().downcast::<NumberFormat>() {
// 3. Return nf.
return Ok(nf);
}
// 2. If nf does not have an [[InitializedNumberFormat]] internal slot and ? OrdinaryHasInstance(%Intl.NumberFormat%, nf)
// is true, then
let constructor = context
.intrinsics()
.constructors()
.number_format()
.constructor();
if JsValue::ordinary_has_instance(&constructor.into(), nf, context)? {
let fallback_symbol = context
.intrinsics()
.objects()
.intl()
.borrow()
.data
.fallback_symbol();
// a. Return ? Get(nf, %Intl%.[[FallbackSymbol]]).
let nf = nf_o.get(fallback_symbol, context)?;
if let JsValue::Object(nf) = nf {
if let Ok(nf) = nf.downcast::<NumberFormat>() {
return Ok(nf);
}
}
}
Err(JsNativeError::typ()
.with_message("object was not an `Intl.NumberFormat` object")
.into())
}
/// Abstract operation [`ToIntlMathematicalValue ( value )`][spec].
///
/// [spec]: https://tc39.es/ecma402/#sec-tointlmathematicalvalue
fn to_intl_mathematical_value(value: &JsValue, context: &mut Context) -> JsResult<FixedDecimal> {
// 1. Let primValue be ? ToPrimitive(value, number).
let prim_value = value.to_primitive(context, PreferredType::Number)?;
// TODO: Add support in `FixedDecimal` for infinity and NaN, which
// should remove the returned errors.
match prim_value {
// 2. If Type(primValue) is BigInt, return ℝ(primValue).
JsValue::BigInt(bi) => {
let bi = bi.to_string();
FixedDecimal::try_from(bi.as_bytes())
.map_err(|err| JsNativeError::range().with_message(err.to_string()).into())
}
// 3. If Type(primValue) is String, then
// a. Let str be primValue.
JsValue::String(s) => {
// 5. Let text be StringToCodePoints(str).
// 6. Let literal be ParseText(text, StringNumericLiteral).
// 7. If literal is a List of errors, return not-a-number.
// 8. Let intlMV be the StringIntlMV of literal.
// 9. If intlMV is a mathematical value, then
// a. Let rounded be RoundMVResult(abs(intlMV)).
// b. If rounded is +∞𝔽 and intlMV < 0, return negative-infinity.
// c. If rounded is +∞𝔽, return positive-infinity.
// d. If rounded is +0𝔽 and intlMV < 0, return negative-zero.
// e. If rounded is +0𝔽, return 0.
js_string_to_fixed_decimal(&s).ok_or_else(|| {
JsNativeError::syntax()
.with_message("could not parse the provided string")
.into()
})
}
// 4. Else,
other => {
// a. Let x be ? ToNumber(primValue).
// b. If x is -0𝔽, return negative-zero.
// c. Let str be Number::toString(x, 10).
let x = other.to_number(context)?;
FixedDecimal::try_from_f64(x, FloatPrecision::Floating)
.map_err(|err| JsNativeError::range().with_message(err.to_string()).into())
}
}
}
/// Abstract operation [`StringToNumber ( str )`][spec], but specialized for the conversion
/// to a `FixedDecimal`.
///
/// [spec]: https://tc39.es/ecma262/#sec-stringtonumber
// TODO: Introduce `Infinity` and `NaN` to `FixedDecimal` to make this operation
// infallible.
pub(crate) fn js_string_to_fixed_decimal(string: &JsString) -> Option<FixedDecimal> {
// 1. Let text be ! StringToCodePoints(str).
// 2. Let literal be ParseText(text, StringNumericLiteral).
let Ok(string) = string.to_std_string() else {
// 3. If literal is a List of errors, return NaN.
return None;
};
// 4. Return StringNumericValue of literal.
let string = string.trim_matches(is_trimmable_whitespace);
match string {
"" => return Some(FixedDecimal::from(0)),
"-Infinity" | "Infinity" | "+Infinity" => return None,
_ => {}
}
let mut s = string.bytes();
let base = match (s.next(), s.next()) {
(Some(b'0'), Some(b'b' | b'B')) => Some(2),
(Some(b'0'), Some(b'o' | b'O')) => Some(8),
(Some(b'0'), Some(b'x' | b'X')) => Some(16),
// Make sure that no further variants of "infinity" are parsed.
(Some(b'i' | b'I'), _) => {
return None;
}
_ => None,
};
// Parse numbers that begin with `0b`, `0o` and `0x`.
let s = if let Some(base) = base {
let string = &string[2..];
if string.is_empty() {
return None;
}
let int = BigInt::from_str_radix(string, base).ok()?;
let int_str = int.to_string();
Cow::Owned(int_str)
} else {
Cow::Borrowed(string)
};
FixedDecimal::try_from(s.as_bytes()).ok()
}

1093
core/engine/src/builtins/intl/number_format/options.rs

File diff suppressed because it is too large Load Diff

41
core/engine/src/builtins/intl/number_format/tests.rs

@ -0,0 +1,41 @@
use crate::builtins::intl::number_format::RoundingIncrement;
use fixed_decimal::RoundingIncrement::*;
#[test]
fn u16_to_rounding_increment_sunny_day() {
#[rustfmt::skip]
let valid_cases: [(u16, RoundingIncrement); 15] = [
// Singles
(1, RoundingIncrement::from_parts(MultiplesOf1, 0).unwrap()),
(2, RoundingIncrement::from_parts(MultiplesOf2, 0).unwrap()),
(5, RoundingIncrement::from_parts(MultiplesOf5, 0).unwrap()),
// Tens
(10, RoundingIncrement::from_parts(MultiplesOf1, 1).unwrap()),
(20, RoundingIncrement::from_parts(MultiplesOf2, 1).unwrap()),
(25, RoundingIncrement::from_parts(MultiplesOf25, 0).unwrap()),
(50, RoundingIncrement::from_parts(MultiplesOf5, 1).unwrap()),
// Hundreds
(100, RoundingIncrement::from_parts(MultiplesOf1, 2).unwrap()),
(200, RoundingIncrement::from_parts(MultiplesOf2, 2).unwrap()),
(250, RoundingIncrement::from_parts(MultiplesOf25, 1).unwrap()),
(500, RoundingIncrement::from_parts(MultiplesOf5, 2).unwrap()),
// Thousands
(1000, RoundingIncrement::from_parts(MultiplesOf1, 3).unwrap()),
(2000, RoundingIncrement::from_parts(MultiplesOf2, 3).unwrap()),
(2500, RoundingIncrement::from_parts(MultiplesOf25, 2).unwrap()),
(5000, RoundingIncrement::from_parts(MultiplesOf5, 3).unwrap()),
];
for (num, increment) in valid_cases {
assert_eq!(RoundingIncrement::from_u16(num), Some(increment));
}
}
#[test]
fn u16_to_rounding_increment_rainy_day() {
const INVALID_CASES: [u16; 9] = [0, 4, 6, 24, 10000, 65535, 7373, 140, 1500];
for num in INVALID_CASES {
assert!(RoundingIncrement::from_u16(num).is_none());
}
}

525
core/engine/src/builtins/intl/number_format/utils.rs

@ -1,525 +0,0 @@
use boa_macros::utf16;
use fixed_decimal::{FixedDecimal, FloatPrecision, RoundingIncrement as BaseMultiple};
use crate::{
builtins::{
intl::{
number_format::{Extrema, RoundingType, TrailingZeroDisplay},
options::{default_number_option, get_number_option},
},
options::{get_option, RoundingMode},
},
Context, JsNativeError, JsObject, JsResult,
};
use super::{DigitFormatOptions, Notation, RoundingPriority};
/// The increment of a rounding operation.
///
/// This differs from [`fixed_decimal::RoundingIncrement`] because ECMA402 accepts
/// several more increments than `fixed_decimal`, but all increments can be decomposed
/// into the target multiple and the magnitude offset.
///
/// For example, rounding the number `0.02456` to the increment 200 at position
/// -3 is equivalent to rounding the same number to the increment 2 at position -1, and adding
/// trailing zeroes.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub(crate) struct RoundingIncrement {
multiple: BaseMultiple,
// INVARIANT: can only be 0, 1, 2, or 3
magnitude_offset: u8,
}
impl RoundingIncrement {
/// Creates a `RoundingIncrement` from the numeric value of the increment.
fn from_u16(increment: u16) -> Option<Self> {
let mut offset = 0u8;
let multiple = loop {
let rem = increment % 10u16.checked_pow(u32::from(offset + 1))?;
if rem != 0 {
break increment / 10u16.pow(u32::from(offset));
}
offset += 1;
};
if offset > 3 {
return None;
}
let multiple = match multiple {
1 => BaseMultiple::MultiplesOf1,
2 => BaseMultiple::MultiplesOf2,
5 => BaseMultiple::MultiplesOf5,
25 => BaseMultiple::MultiplesOf25,
_ => return None,
};
Some(RoundingIncrement {
multiple,
magnitude_offset: offset,
})
}
/// Gets the numeric value of this `RoundingIncrement`.
pub(crate) fn to_u16(self) -> u16 {
u16::from(self.magnitude_offset + 1)
* match self.multiple {
BaseMultiple::MultiplesOf1 => 1,
BaseMultiple::MultiplesOf2 => 2,
BaseMultiple::MultiplesOf5 => 5,
BaseMultiple::MultiplesOf25 => 25,
_ => {
debug_assert!(false, "base multiples can only be 1, 2, 5, or 25");
1
}
}
}
/// Gets the magnitude offset that needs to be added to the rounding position
/// for this rounding increment.
fn magnitude_offset(self) -> i16 {
i16::from(self.magnitude_offset)
}
}
/// Abstract operation [`SetNumberFormatDigitOptions ( intlObj, options, mnfdDefault, mxfdDefault, notation )`][spec].
///
/// Gets the digit format options of the number formatter from the options object and the requested notation.
///
/// [spec]: https://tc39.es/ecma402/#sec-setnfdigitoptions
pub(crate) fn get_digit_format_options(
options: &JsObject,
min_float_digits_default: u8,
mut max_float_digits_default: u8,
notation: Notation,
context: &mut Context,
) -> JsResult<DigitFormatOptions> {
// 1. Let mnid be ? GetNumberOption(options, "minimumIntegerDigits,", 1, 21, 1).
let minimum_integer_digits =
get_number_option(options, utf16!("minimumIntegerDigits"), 1, 21, context)?.unwrap_or(1);
// 2. Let mnfd be ? Get(options, "minimumFractionDigits").
let min_float_digits = options.get(utf16!("minimumFractionDigits"), context)?;
// 3. Let mxfd be ? Get(options, "maximumFractionDigits").
let max_float_digits = options.get(utf16!("maximumFractionDigits"), context)?;
// 4. Let mnsd be ? Get(options, "minimumSignificantDigits").
let min_sig_digits = options.get(utf16!("minimumSignificantDigits"), context)?;
// 5. Let mxsd be ? Get(options, "maximumSignificantDigits").
let max_sig_digits = options.get(utf16!("maximumSignificantDigits"), context)?;
// 7. Let roundingPriority be ? GetOption(options, "roundingPriority", string, « "auto", "morePrecision", "lessPrecision" », "auto").
let mut rounding_priority =
get_option(options, utf16!("roundingPriority"), context)?.unwrap_or_default();
// 8. Let roundingIncrement be ? GetNumberOption(options, "roundingIncrement", 1, 5000, 1).
// 9. If roundingIncrement is not in « 1, 2, 5, 10, 20, 25, 50, 100, 200, 250, 500, 1000, 2000, 2500, 5000 », throw a RangeError exception.
let rounding_increment =
get_number_option(options, utf16!("roundingIncrement"), 1, 5000, context)?.unwrap_or(1);
let rounding_increment = RoundingIncrement::from_u16(rounding_increment).ok_or_else(|| {
JsNativeError::range().with_message("invalid value for option `roundingIncrement`")
})?;
// 10. Let roundingMode be ? GetOption(options, "roundingMode", string, « "ceil", "floor", "expand", "trunc", "halfCeil", "halfFloor", "halfExpand", "halfTrunc", "halfEven" », "halfExpand").
let rounding_mode = get_option(options, utf16!("roundingMode"), context)?.unwrap_or_default();
// 11. Let trailingZeroDisplay be ? GetOption(options, "trailingZeroDisplay", string, « "auto", "stripIfInteger" », "auto").
let trailing_zero_display =
get_option(options, utf16!("trailingZeroDisplay"), context)?.unwrap_or_default();
// 12. NOTE: All fields required by SetNumberFormatDigitOptions have now been read from options. The remainder of this AO interprets the options and may throw exceptions.
// 13. If roundingIncrement is not 1, set mxfdDefault to mnfdDefault.
if rounding_increment.to_u16() != 1 {
max_float_digits_default = min_float_digits_default;
}
// 17. If mnsd is not undefined or mxsd is not undefined, then
// a. Let hasSd be true.
// 18. Else,
// a. Let hasSd be false.
let has_sig_limits = !min_sig_digits.is_undefined() || !max_sig_digits.is_undefined();
// 19. If mnfd is not undefined or mxfd is not undefined, then
// a. Let hasFd be true.
// 20. Else,
// a. Let hasFd be false.
let has_float_limits = !min_float_digits.is_undefined() || !max_float_digits.is_undefined();
// 21. Let needSd be true.
// 22. Let needFd be true.
let (need_sig_limits, need_frac_limits) = if rounding_priority == RoundingPriority::Auto {
// 23. If roundingPriority is "auto", then
// a. Set needSd to hasSd.
// b. If needSd is true, or hasFd is false and notation is "compact", then
// i. Set needFd to false.
(
has_sig_limits,
!has_sig_limits && (has_float_limits || notation != Notation::Compact),
)
} else {
(true, true)
};
// 24. If needSd is true, then
let sig_digits = if need_sig_limits {
// a. If hasSd is true, then
let extrema = if has_sig_limits {
// i. Set intlObj.[[MinimumSignificantDigits]] to ? DefaultNumberOption(mnsd, 1, 21, 1).
let min_sig = default_number_option(&min_sig_digits, 1, 21, context)?.unwrap_or(1);
// ii. Set intlObj.[[MaximumSignificantDigits]] to ? DefaultNumberOption(mxsd, intlObj.[[MinimumSignificantDigits]], 21, 21).
let max_sig =
default_number_option(&max_sig_digits, min_sig, 21, context)?.unwrap_or(21);
Extrema {
minimum: min_sig,
maximum: max_sig,
}
} else {
// b. Else,
Extrema {
// i. Set intlObj.[[MinimumSignificantDigits]] to 1.
minimum: 1,
// ii. Set intlObj.[[MaximumSignificantDigits]] to 21.
maximum: 21,
}
};
assert!(extrema.minimum <= extrema.maximum);
Some(extrema)
} else {
None
};
// 25. If needFd is true, then
let fractional_digits = if need_frac_limits {
// a. If hasFd is true, then
let extrema = if has_float_limits {
// i. Set mnfd to ? DefaultNumberOption(mnfd, 0, 100, undefined).
let min_float_digits = default_number_option(&min_float_digits, 0, 100, context)?;
// ii. Set mxfd to ? DefaultNumberOption(mxfd, 0, 100, undefined).
let max_float_digits = default_number_option(&max_float_digits, 0, 100, context)?;
let (min_float_digits, max_float_digits) = match (min_float_digits, max_float_digits) {
(Some(min_float_digits), Some(max_float_digits)) => {
// v. Else if mnfd is greater than mxfd, throw a RangeError exception.
if min_float_digits > max_float_digits {
return Err(JsNativeError::range().with_message(
"`minimumFractionDigits` cannot be bigger than `maximumFractionDigits`",
).into());
}
(min_float_digits, max_float_digits)
}
// iv. Else if mxfd is undefined, set mxfd to max(mxfdDefault, mnfd).
(Some(min_float_digits), None) => (
min_float_digits,
u8::max(max_float_digits_default, min_float_digits),
),
// iii. If mnfd is undefined, set mnfd to min(mnfdDefault, mxfd).
(None, Some(max_float_digits)) => (
u8::min(min_float_digits_default, max_float_digits),
max_float_digits,
),
(None, None) => {
unreachable!("`has_fd` can only be true if `mnfd` or `mxfd` is not undefined")
}
};
Extrema {
// vi. Set intlObj.[[MinimumFractionDigits]] to mnfd.
minimum: min_float_digits,
// vii. Set intlObj.[[MaximumFractionDigits]] to mxfd.
maximum: max_float_digits,
}
} else {
// b. Else,
Extrema {
// i. Set intlObj.[[MinimumFractionDigits]] to mnfdDefault.
minimum: min_float_digits_default,
// ii. Set intlObj.[[MaximumFractionDigits]] to mxfdDefault.
maximum: max_float_digits_default,
}
};
assert!(extrema.minimum <= extrema.maximum);
Some(extrema)
} else {
None
};
let rounding_type = match (sig_digits, fractional_digits) {
// 26. If needSd is false and needFd is false, then
(None, None) => {
// f. Set intlObj.[[ComputedRoundingPriority]] to "morePrecision".
rounding_priority = RoundingPriority::MorePrecision;
// e. Set intlObj.[[RoundingType]] to morePrecision.
RoundingType::MorePrecision {
significant_digits: Extrema {
// c. Set intlObj.[[MinimumSignificantDigits]] to 1.
minimum: 1,
// d. Set intlObj.[[MaximumSignificantDigits]] to 2.
maximum: 2,
},
fraction_digits: Extrema {
// a. Set intlObj.[[MinimumFractionDigits]] to 0.
minimum: 0,
// b. Set intlObj.[[MaximumFractionDigits]] to 0.
maximum: 0,
},
}
}
(Some(significant_digits), Some(fraction_digits)) => match rounding_priority {
RoundingPriority::MorePrecision => RoundingType::MorePrecision {
significant_digits,
fraction_digits,
},
RoundingPriority::LessPrecision => RoundingType::LessPrecision {
significant_digits,
fraction_digits,
},
RoundingPriority::Auto => {
unreachable!("Cannot have both roundings when the priority is `Auto`")
}
},
(Some(sig), None) => RoundingType::SignificantDigits(sig),
(None, Some(frac)) => RoundingType::FractionDigits(frac),
};
if rounding_increment.to_u16() != 1 {
let RoundingType::FractionDigits(range) = rounding_type else {
return Err(JsNativeError::typ()
.with_message("option `roundingIncrement` invalid for the current set of options")
.into());
};
if range.minimum != range.maximum {
return Err(JsNativeError::range()
.with_message("option `roundingIncrement` invalid for the current set of options")
.into());
}
}
Ok(DigitFormatOptions {
// 6. Set intlObj.[[MinimumIntegerDigits]] to mnid.
minimum_integer_digits,
// 14. Set intlObj.[[RoundingIncrement]] to roundingIncrement.
rounding_increment,
// 15. Set intlObj.[[RoundingMode]] to roundingMode.
rounding_mode,
// 16. Set intlObj.[[TrailingZeroDisplay]] to trailingZeroDisplay.
trailing_zero_display,
rounding_type,
rounding_priority,
})
}
/// Abstract operation [`FormatNumericToString ( intlObject, x )`][spec].
///
/// Converts the input number to a `FixedDecimal` with the specified digit format options.
///
/// [spec]: https://tc39.es/ecma402/#sec-formatnumberstring
pub(crate) fn f64_to_formatted_fixed_decimal(
number: f64,
options: &DigitFormatOptions,
) -> FixedDecimal {
fn round(number: &mut FixedDecimal, position: i16, mode: RoundingMode, multiple: BaseMultiple) {
match mode {
RoundingMode::Ceil => number.ceil_to_increment(position, multiple),
RoundingMode::Floor => number.floor_to_increment(position, multiple),
RoundingMode::Expand => number.expand_to_increment(position, multiple),
RoundingMode::Trunc => number.trunc_to_increment(position, multiple),
RoundingMode::HalfCeil => number.half_ceil_to_increment(position, multiple),
RoundingMode::HalfFloor => number.half_floor_to_increment(position, multiple),
RoundingMode::HalfExpand => number.half_expand_to_increment(position, multiple),
RoundingMode::HalfTrunc => number.half_trunc_to_increment(position, multiple),
RoundingMode::HalfEven => number.half_even_to_increment(position, multiple),
}
}
// <https://tc39.es/ecma402/#sec-torawprecision>
fn to_raw_precision(
number: &mut FixedDecimal,
min_precision: u8,
max_precision: u8,
rounding_mode: RoundingMode,
) -> i16 {
let msb = *number.magnitude_range().end();
let min_msb = msb - i16::from(min_precision) + 1;
let max_msb = msb - i16::from(max_precision) + 1;
number.pad_end(min_msb);
round(number, max_msb, rounding_mode, BaseMultiple::MultiplesOf1);
max_msb
}
// <https://tc39.es/ecma402/#sec-torawfixed>
fn to_raw_fixed(
number: &mut FixedDecimal,
min_fraction: u8,
max_fraction: u8,
rounding_increment: RoundingIncrement,
rounding_mode: RoundingMode,
) -> i16 {
#[cfg(debug_assertions)]
if rounding_increment.to_u16() != 1 {
assert_eq!(min_fraction, max_fraction);
}
number.pad_end(-i16::from(min_fraction));
round(
number,
rounding_increment.magnitude_offset() - i16::from(max_fraction),
rounding_mode,
rounding_increment.multiple,
);
-i16::from(max_fraction)
}
// 1. If x is negative-zero, then
// a. Let isNegative be true.
// b. Set x to 0.
// 2. Else,
// a. Assert: x is a mathematical value.
// b. If x < 0, let isNegative be true; else let isNegative be false.
// c. If isNegative is true, then
// i. Set x to -x.
// We can skip these steps, because `FixedDecimal` already provides support for
// negative zeroes.
let mut number = FixedDecimal::try_from_f64(number, FloatPrecision::Floating)
.expect("`number` must be finite");
// 3. Let unsignedRoundingMode be GetUnsignedRoundingMode(intlObject.[[RoundingMode]], isNegative).
// Skipping because `FixedDecimal`'s API already provides methods equivalent to `RoundingMode`s.
match options.rounding_type {
// 4. If intlObject.[[RoundingType]] is significantDigits, then
RoundingType::SignificantDigits(Extrema { minimum, maximum }) => {
// a. Let result be ToRawPrecision(x, intlObject.[[MinimumSignificantDigits]], intlObject.[[MaximumSignificantDigits]], unsignedRoundingMode).
to_raw_precision(&mut number, minimum, maximum, options.rounding_mode);
}
// 5. Else if intlObject.[[RoundingType]] is fractionDigits, then
RoundingType::FractionDigits(Extrema { minimum, maximum }) => {
// a. Let result be ToRawFixed(x, intlObject.[[MinimumFractionDigits]], intlObject.[[MaximumFractionDigits]], intlObject.[[RoundingIncrement]], unsignedRoundingMode).
to_raw_fixed(
&mut number,
minimum,
maximum,
options.rounding_increment,
options.rounding_mode,
);
}
// 6. Else,
RoundingType::MorePrecision {
significant_digits,
fraction_digits,
}
| RoundingType::LessPrecision {
significant_digits,
fraction_digits,
} => {
let prefer_more_precision =
matches!(options.rounding_type, RoundingType::MorePrecision { .. });
// a. Let sResult be ToRawPrecision(x, intlObject.[[MinimumSignificantDigits]], intlObject.[[MaximumSignificantDigits]], unsignedRoundingMode).
let mut fixed = number.clone();
let s_magnitude = to_raw_precision(
&mut number,
significant_digits.maximum,
significant_digits.minimum,
options.rounding_mode,
);
// b. Let fResult be ToRawFixed(x, intlObject.[[MinimumFractionDigits]], intlObject.[[MaximumFractionDigits]], intlObject.[[RoundingIncrement]], unsignedRoundingMode).
let f_magnitude = to_raw_fixed(
&mut fixed,
fraction_digits.maximum,
fraction_digits.minimum,
options.rounding_increment,
options.rounding_mode,
);
// c. If intlObject.[[RoundingType]] is morePrecision, then
// i. If sResult.[[RoundingMagnitude]] ≤ fResult.[[RoundingMagnitude]], then
// 1. Let result be sResult.
// ii. Else,
// 1. Let result be fResult.
// d. Else,
// i. Assert: intlObject.[[RoundingType]] is lessPrecision.
// ii. If sResult.[[RoundingMagnitude]] ≤ fResult.[[RoundingMagnitude]], then
// 1. Let result be fResult.
// iii. Else,
// 1. Let result be sResult.
if (prefer_more_precision && f_magnitude < s_magnitude)
|| (!prefer_more_precision && s_magnitude <= f_magnitude)
{
number = fixed;
}
}
}
// 7. Set x to result.[[RoundedNumber]].
// 8. Let string be result.[[FormattedString]].
// 9. If intlObject.[[TrailingZeroDisplay]] is "stripIfInteger" and x modulo 1 = 0, then
if options.trailing_zero_display == TrailingZeroDisplay::StripIfInteger
&& number.nonzero_magnitude_end() >= 0
{
// a. Let i be StringIndexOf(string, ".", 0).
// b. If i ≠ -1, set string to the substring of string from 0 to i.
number.trim_end();
}
// 10. Let int be result.[[IntegerDigitsCount]].
// 11. Let minInteger be intlObject.[[MinimumIntegerDigits]].
// 12. If int < minInteger, then
// a. Let forwardZeros be the String consisting of minInteger - int occurrences of the code unit 0x0030 (DIGIT ZERO).
// b. Set string to the string-concatenation of forwardZeros and string.
number.pad_start(i16::from(options.minimum_integer_digits));
// 13. If isNegative is true, then
// a. If x is 0, set x to negative-zero. Otherwise, set x to -x.
// As mentioned above, `FixedDecimal` has support for this.
// 14. Return the Record { [[RoundedNumber]]: x, [[FormattedString]]: string }.
number
}
#[cfg(test)]
mod tests {
use crate::builtins::intl::number_format::RoundingIncrement;
use fixed_decimal::RoundingIncrement::*;
#[test]
fn u16_to_rounding_increment_sunny_day() {
#[rustfmt::skip]
const VALID_CASES: [(u16, RoundingIncrement); 15] = [
// Singles
(1, RoundingIncrement { multiple: MultiplesOf1, magnitude_offset: 0 }),
(2, RoundingIncrement { multiple: MultiplesOf2, magnitude_offset: 0 }),
(5, RoundingIncrement { multiple: MultiplesOf5, magnitude_offset: 0 }),
// Tens
(10, RoundingIncrement { multiple: MultiplesOf1, magnitude_offset: 1 }),
(20, RoundingIncrement { multiple: MultiplesOf2, magnitude_offset: 1 }),
(25, RoundingIncrement { multiple: MultiplesOf25, magnitude_offset: 0 }),
(50, RoundingIncrement { multiple: MultiplesOf5, magnitude_offset: 1 }),
// Hundreds
(100, RoundingIncrement { multiple: MultiplesOf1, magnitude_offset: 2 }),
(200, RoundingIncrement { multiple: MultiplesOf2, magnitude_offset: 2 }),
(250, RoundingIncrement { multiple: MultiplesOf25, magnitude_offset: 1 }),
(500, RoundingIncrement { multiple: MultiplesOf5, magnitude_offset: 2 }),
// Thousands
(1000, RoundingIncrement { multiple: MultiplesOf1, magnitude_offset: 3 }),
(2000, RoundingIncrement { multiple: MultiplesOf2, magnitude_offset: 3 }),
(2500, RoundingIncrement { multiple: MultiplesOf25, magnitude_offset: 2 }),
(5000, RoundingIncrement { multiple: MultiplesOf5, magnitude_offset: 3 }),
];
for (num, increment) in VALID_CASES {
assert_eq!(RoundingIncrement::from_u16(num), Some(increment));
}
}
#[test]
fn u16_to_rounding_increment_rainy_day() {
const INVALID_CASES: [u16; 9] = [0, 4, 6, 24, 10000, 65535, 7373, 140, 1500];
for num in INVALID_CASES {
assert!(RoundingIncrement::from_u16(num).is_none());
}
}
}

18
core/engine/src/builtins/intl/plural_rules/mod.rs

@ -27,10 +27,7 @@ use crate::{
use super::{
locale::{canonicalize_locale_list, resolve_locale, supported_locales},
number_format::{
f64_to_formatted_fixed_decimal, get_digit_format_options, DigitFormatOptions, Extrema,
Notation,
},
number_format::{DigitFormatOptions, Extrema, NotationKind},
options::{coerce_options_to_object, IntlOptions},
Service,
};
@ -132,7 +129,8 @@ impl BuiltInConstructor for PluralRules {
get_option(&options, utf16!("type"), context)?.unwrap_or(PluralRuleType::Cardinal);
// 8. Perform ? SetNumberFormatDigitOptions(pluralRules, options, +0𝔽, 3𝔽, "standard").
let format_options = get_digit_format_options(&options, 0, 3, Notation::Standard, context)?;
let format_options =
DigitFormatOptions::from_options(&options, 0, 3, NotationKind::Standard, context)?;
// 9. Let localeData be %PluralRules%.[[LocaleData]].
// 10. Let r be ResolveLocale(%PluralRules%.[[AvailableLocales]], requestedLocales, opt, %PluralRules%.[[RelevantExtensionKeys]], localeData).
@ -388,7 +386,7 @@ impl PluralRules {
options
.property(
js_string!("roundingMode"),
js_string!(plural_rules.format_options.rounding_mode.to_string()),
js_string!(plural_rules.format_options.rounding_mode.to_js_string()),
Attribute::all(),
)
.property(
@ -398,10 +396,10 @@ impl PluralRules {
)
.property(
js_string!("trailingZeroDisplay"),
js_string!(plural_rules
plural_rules
.format_options
.trailing_zero_display
.to_string()),
.to_js_string(),
Attribute::all(),
);
@ -431,7 +429,7 @@ impl PluralRules {
// a. Perform ! CreateDataPropertyOrThrow(options, "roundingPriority", "auto").
options.property(
js_string!("roundingPriority"),
js_string!(plural_rules.format_options.rounding_priority.to_string()),
js_string!(plural_rules.format_options.rounding_priority.to_js_string()),
Attribute::all(),
);
@ -468,7 +466,7 @@ fn resolve_plural(plural_rules: &PluralRules, n: f64) -> ResolvedPlural {
// 5. Let locale be pluralRules.[[Locale]].
// 6. Let type be pluralRules.[[Type]].
// 7. Let res be ! FormatNumericToString(pluralRules, n).
let fixed = f64_to_formatted_fixed_decimal(n, &plural_rules.format_options);
let fixed = plural_rules.format_options.format_f64(n);
// 8. Let s be res.[[FormattedString]].
// 9. Let operands be ! GetOperands(s).

1
core/engine/src/builtins/mod.rs

@ -279,6 +279,7 @@ impl Realm {
intl::segmenter::Segments::init(self);
intl::segmenter::SegmentIterator::init(self);
intl::PluralRules::init(self);
intl::NumberFormat::init(self);
}
#[cfg(feature = "temporal")]

35
core/engine/src/builtins/options.rs

@ -138,6 +138,24 @@ pub(crate) enum RoundingMode {
HalfEven,
}
impl RoundingMode {
#[cfg(feature = "intl")]
pub(crate) fn to_js_string(self) -> JsString {
use crate::js_string;
match self {
Self::Ceil => js_string!("ceil"),
Self::Floor => js_string!("floor"),
Self::Expand => js_string!("expand"),
Self::Trunc => js_string!("trunc"),
Self::HalfCeil => js_string!("halfCeil"),
Self::HalfFloor => js_string!("halfFloor"),
Self::HalfExpand => js_string!("halfExpand"),
Self::HalfTrunc => js_string!("halfTrunc"),
Self::HalfEven => js_string!("halfEven"),
}
}
}
#[derive(Debug)]
pub(crate) struct ParseRoundingModeError;
@ -168,23 +186,6 @@ impl FromStr for RoundingMode {
impl ParsableOptionType for RoundingMode {}
impl fmt::Display for RoundingMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Ceil => "ceil",
Self::Floor => "floor",
Self::Expand => "expand",
Self::Trunc => "trunc",
Self::HalfCeil => "halfCeil",
Self::HalfFloor => "halfFloor",
Self::HalfExpand => "halfExpand",
Self::HalfTrunc => "halfTrunc",
Self::HalfEven => "halfEven",
}
.fmt(f)
}
}
// TODO: remove once confirmed.
#[cfg(feature = "temporal")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]

59
core/engine/src/context/intrinsics.rs

@ -14,6 +14,9 @@ use crate::{
JsSymbol,
};
#[cfg(feature = "intl")]
use crate::builtins::intl::Intl;
/// The intrinsic objects and constructors.
///
/// `Intrinsics` is internally stored using a `Gc`, which makes it cheapily clonable
@ -29,15 +32,22 @@ pub struct Intrinsics {
}
impl Intrinsics {
pub(crate) fn new(root_shape: &RootShape) -> Self {
/// Creates a new set of uninitialized intrinsics.
///
/// Creates all the required empty objects for every intrinsic in this realm.
///
/// To initialize all the intrinsics with their spec properties, see [`Realm::initialize`].
///
/// [`Realm::initialize`]: crate::realm::Realm::initialize
pub(crate) fn uninit(root_shape: &RootShape) -> Option<Self> {
let constructors = StandardConstructors::default();
let templates = ObjectTemplates::new(root_shape, &constructors);
Self {
Some(Self {
constructors,
objects: IntrinsicObjects::default(),
objects: IntrinsicObjects::uninit()?,
templates,
}
})
}
/// Return the cached intrinsic objects.
@ -168,6 +178,8 @@ pub struct StandardConstructors {
segmenter: StandardConstructor,
#[cfg(feature = "intl")]
plural_rules: StandardConstructor,
#[cfg(feature = "intl")]
number_format: StandardConstructor,
#[cfg(feature = "temporal")]
instant: StandardConstructor,
#[cfg(feature = "temporal")]
@ -258,6 +270,8 @@ impl Default for StandardConstructors {
segmenter: StandardConstructor::default(),
#[cfg(feature = "intl")]
plural_rules: StandardConstructor::default(),
#[cfg(feature = "intl")]
number_format: StandardConstructor::default(),
#[cfg(feature = "temporal")]
instant: StandardConstructor::default(),
#[cfg(feature = "temporal")]
@ -876,6 +890,19 @@ impl StandardConstructors {
&self.plural_rules
}
/// Returns the `Intl.NumberFormat` constructor.
///
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma402/#sec-intl.numberformat
#[inline]
#[must_use]
#[cfg(feature = "intl")]
pub const fn number_format(&self) -> &StandardConstructor {
&self.number_format
}
/// Returns the `Temporal.Instant` constructor.
///
/// More information:
@ -1068,7 +1095,7 @@ pub struct IntrinsicObjects {
/// [`%Intl%`](https://tc39.es/ecma402/#intl-object)
#[cfg(feature = "intl")]
intl: JsObject,
intl: JsObject<Intl>,
/// [`%SegmentsPrototype%`](https://tc39.es/ecma402/#sec-%segmentsprototype%-object)
#[cfg(feature = "intl")]
@ -1083,9 +1110,17 @@ pub struct IntrinsicObjects {
now: JsObject,
}
impl Default for IntrinsicObjects {
fn default() -> Self {
Self {
impl IntrinsicObjects {
/// Creates a new set of uninitialized intrinsic objects.
///
/// Creates all the required empty objects for every intrinsic object in this realm.
///
/// To initialize all the intrinsic objects with their spec properties, see [`Realm::initialize`].
///
/// [`Realm::initialize`]: crate::realm::Realm::initialize
#[allow(clippy::unnecessary_wraps)]
pub(crate) fn uninit() -> Option<Self> {
Some(Self {
reflect: JsObject::default(),
math: JsObject::default(),
json: JsObject::default(),
@ -1107,18 +1142,16 @@ impl Default for IntrinsicObjects {
#[cfg(feature = "annex-b")]
unescape: JsFunction::empty_intrinsic_function(false),
#[cfg(feature = "intl")]
intl: JsObject::default(),
intl: JsObject::new_unique(None, Intl::new()?),
#[cfg(feature = "intl")]
segments_prototype: JsObject::default(),
#[cfg(feature = "temporal")]
temporal: JsObject::default(),
#[cfg(feature = "temporal")]
now: JsObject::default(),
})
}
}
}
impl IntrinsicObjects {
/// Gets the [`%ThrowTypeError%`][spec] intrinsic function.
///
/// [spec]: https://tc39.es/ecma262/#sec-%throwtypeerror%
@ -1285,7 +1318,7 @@ impl IntrinsicObjects {
#[must_use]
#[cfg(feature = "intl")]
#[inline]
pub fn intl(&self) -> JsObject {
pub fn intl(&self) -> JsObject<Intl> {
self.intl.clone()
}

4
core/engine/src/context/mod.rs

@ -529,7 +529,7 @@ impl Context {
/// Create a new Realm with the default global bindings.
pub fn create_realm(&mut self) -> JsResult<Realm> {
let realm = Realm::create(self.host_hooks, &self.root_shape);
let realm = Realm::create(self.host_hooks, &self.root_shape)?;
let old_realm = self.enter_realm(realm);
@ -1030,7 +1030,7 @@ impl ContextBuilder {
let root_shape = RootShape::default();
let host_hooks = self.host_hooks.unwrap_or(&DefaultHooks);
let realm = Realm::create(host_hooks, &root_shape);
let realm = Realm::create(host_hooks, &root_shape)?;
let vm = Vm::new(realm);
let module_loader: Rc<dyn ModuleLoader> = if let Some(loader) = self.module_loader {

22
core/engine/src/object/jsobject.rs

@ -597,6 +597,28 @@ impl<T: NativeObject + ?Sized> JsObject<T> {
Self { inner }
}
/// Creates a new `JsObject` from prototype, and data.
///
/// Note that the returned object will not be erased to be convertible to a
/// `JsValue`. To erase the pointer, call [`JsObject::upcast`].
pub fn new_unique<O: Into<Option<JsObject>>>(prototype: O, data: T) -> Self
where
T: Sized,
{
let internal_methods = data.internal_methods();
let inner = Gc::new(VTableObject {
object: GcRefCell::new(Object {
data,
properties: PropertyMap::from_prototype_unique_shape(prototype.into()),
extensible: true,
private_elements: ThinVec::new(),
}),
vtable: internal_methods,
});
Self { inner }
}
/// Upcasts this object's inner data from a specific type `T` to an erased type
/// `dyn NativeObject`.
#[must_use]

6
core/engine/src/object/operations.rs

@ -1302,11 +1302,7 @@ impl JsValue {
if let Some(bound_function) = function.downcast_ref::<BoundFunction>() {
// a. Let BC be C.[[BoundTargetFunction]].
// b. Return ? InstanceofOperator(O, BC).
return Self::instance_of(
object,
&bound_function.target_function().clone().into(),
context,
);
return object.instance_of(&bound_function.target_function().clone().into(), context);
}
let Some(mut object) = object.as_object().cloned() else {

11
core/engine/src/realm.rs

@ -19,7 +19,7 @@ use crate::{
environments::DeclarativeEnvironment,
module::Module,
object::shape::RootShape,
HostDefined, JsObject, JsString,
HostDefined, JsNativeError, JsObject, JsResult, JsString,
};
use boa_gc::{Finalize, Gc, GcRef, GcRefCell, GcRefMut, Trace};
use boa_profiler::Profiler;
@ -67,10 +67,13 @@ struct Inner {
impl Realm {
/// Create a new [`Realm`].
#[inline]
pub fn create(hooks: &dyn HostHooks, root_shape: &RootShape) -> Self {
pub fn create(hooks: &dyn HostHooks, root_shape: &RootShape) -> JsResult<Self> {
let _timer = Profiler::global().start_event("Realm::create", "realm");
let intrinsics = Intrinsics::new(root_shape);
let intrinsics = Intrinsics::uninit(root_shape).ok_or_else(|| {
JsNativeError::typ().with_message("failed to create the realm intrinsics")
})?;
let global_object = hooks.create_global_object(&intrinsics);
let global_this = hooks
.create_global_this(&intrinsics)
@ -92,7 +95,7 @@ impl Realm {
realm.initialize();
realm
Ok(realm)
}
/// Gets the intrinsics of this `Realm`.

2
core/engine/src/string/common.rs

@ -140,6 +140,7 @@ impl StaticJsStrings {
(PLURAL_RULES, "PluralRules"),
(SEGMENTER, "Segmenter"),
(DATE_TIME_FORMAT, "DateTimeFormat"),
(NUMBER_FORMAT, "NumberFormat"),
(JSON, "JSON"),
(MAP, "Map"),
(MATH, "Math"),
@ -282,6 +283,7 @@ const RAW_STATICS: &[&[u16]] = &[
utf16!("PluralRules"),
utf16!("Segmenter"),
utf16!("DateTimeFormat"),
utf16!("NumberFormat"),
utf16!("JSON"),
utf16!("Map"),
utf16!("Math"),

6
test262_config.toml

@ -1,4 +1,4 @@
commit = "584048ed081d85f5eed6e884a7b40b6f4bcd67d7"
commit = "f742eb092df835fd07769bbb3537232d3efb61ed"
[ignored]
# Not implemented yet:
@ -10,11 +10,9 @@ features = [
"FinalizationRegistry",
"IsHTMLDDA",
"symbols-as-weakmap-keys",
"intl-normative-optional",
"Intl.DisplayNames",
"Intl.RelativeTimeFormat",
"Intl-enumeration",
"Intl.NumberFormat-v3",
"regexp-v-flag",
### Pending proposals
@ -64,4 +62,4 @@ features = [
"caller",
]
tests = ["NumberFormat"]
tests = []

Loading…
Cancel
Save