From cf613c9e6530844ac46e27544c891b99e8f36d81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Juli=C3=A1n=20Espina?= Date: Mon, 12 Feb 2024 10:46:08 +0000 Subject: [PATCH] Implement prototype of `NumberFormat` (#3669) * Implement prototype of `NumberFormat` * Fix clippy * Misc fixes * Apply review --- Cargo.lock | 2 + Cargo.toml | 2 + core/engine/Cargo.toml | 9 +- core/engine/src/builtins/intl/collator/mod.rs | 2 +- core/engine/src/builtins/intl/mod.rs | 36 +- .../src/builtins/intl/number_format/mod.rs | 851 ++++++++++++- .../builtins/intl/number_format/options.rs | 1093 ++++++++++++++++- .../src/builtins/intl/number_format/tests.rs | 41 + .../src/builtins/intl/number_format/utils.rs | 525 -------- .../src/builtins/intl/plural_rules/mod.rs | 18 +- core/engine/src/builtins/mod.rs | 1 + core/engine/src/builtins/options.rs | 35 +- core/engine/src/context/intrinsics.rs | 59 +- core/engine/src/context/mod.rs | 4 +- core/engine/src/object/jsobject.rs | 22 + core/engine/src/object/operations.rs | 6 +- core/engine/src/realm.rs | 11 +- core/engine/src/string/common.rs | 2 + test262_config.toml | 6 +- 19 files changed, 2109 insertions(+), 616 deletions(-) create mode 100644 core/engine/src/builtins/intl/number_format/tests.rs delete mode 100644 core/engine/src/builtins/intl/number_format/utils.rs diff --git a/Cargo.lock b/Cargo.lock index a92ed3c9b6..7e3aadcc4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 2aa60c8aa2..87bee50cc0 100644 --- a/Cargo.toml +++ b/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" diff --git a/core/engine/Cargo.toml b/core/engine/Cargo.toml index 5288874b08..2c89ed2f7e 100644 --- a/core/engine/Cargo.toml +++ b/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 } diff --git a/core/engine/src/builtins/intl/collator/mod.rs b/core/engine/src/builtins/intl/collator/mod.rs index 2b4127a0cc..b1817b762e 100644 --- a/core/engine/src/builtins/intl/collator/mod.rs +++ b/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, numeric: Option, case_first: Option, diff --git a/core/engine/src/builtins/intl/mod.rs b/core/engine/src/builtins/intl/mod.rs index 1a29e37110..149babe56b 100644 --- a/core/engine/src/builtins/intl/mod.rs +++ b/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 { + 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() } } diff --git a/core/engine/src/builtins/intl/number_format/mod.rs b/core/engine/src/builtins/intl/number_format/mod.rs index 81f115781c..66bbea6283 100644 --- a/core/engine/src/builtins/intl/number_format/mod.rs +++ b/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, + unit_options: UnitFormatOptions, + digit_options: DigitFormatOptions, + notation: Notation, + use_grouping: GroupingStrategy, + sign_display: SignDisplay, + bound_format: Option, +} + +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, +} + +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::(locale.id.clone(), key!("nu"), nu, provider) + }) + .or_else(|| { + locale + .extensions + .unicode + .keywords + .get(&key!("nu")) + .cloned() + .filter(|nu| { + validate_extension::( + 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::(), "init"); + + let get_format = BuiltInBuilder::callable(realm, Self::get_format) + .name(js_string!("get format")) + .build(); + + BuiltInBuilder::from_standard_constructor::(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 { + 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::( + &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 ) + // + + // 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 ) + // + + 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 { + 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::<::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 { + // 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 + // + 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 { + // 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> { + // 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::() { + // 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::() { + 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 { + // 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 { + // 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() +} diff --git a/core/engine/src/builtins/intl/number_format/options.rs b/core/engine/src/builtins/intl/number_format/options.rs index c3dfb8dba6..74dac33194 100644 --- a/core/engine/src/builtins/intl/number_format/options.rs +++ b/core/engine/src/builtins/intl/number_format/options.rs @@ -1,8 +1,468 @@ use std::fmt; -use crate::builtins::options::{ParsableOptionType, RoundingMode}; +use fixed_decimal::{FixedDecimal, FloatPrecision, RoundingIncrement as BaseMultiple, SignDisplay}; -use super::RoundingIncrement; +use boa_macros::utf16; +use tinystr::TinyAsciiStr; + +use crate::{ + builtins::{ + intl::options::{default_number_option, get_number_option}, + options::{get_option, OptionType, ParsableOptionType, RoundingMode}, + }, + js_string, Context, JsNativeError, JsObject, JsResult, JsString, +}; + +#[derive(Debug, Copy, Clone, Default, Eq, PartialEq)] +pub(crate) enum Style { + #[default] + Decimal, + Percent, + Currency, + Unit, +} + +impl Style { + pub(crate) fn to_js_string(self) -> JsString { + match self { + Style::Decimal => js_string!("decimal"), + Style::Percent => js_string!("percent"), + Style::Currency => js_string!("currency"), + Style::Unit => js_string!("unit"), + } + } +} + +#[derive(Debug)] +pub(crate) struct ParseStyleError; + +impl fmt::Display for ParseStyleError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("provided string was not a valid style option") + } +} + +impl std::str::FromStr for Style { + type Err = ParseStyleError; + + fn from_str(s: &str) -> Result { + match s { + "decimal" => Ok(Self::Decimal), + "percent" => Ok(Self::Percent), + "currency" => Ok(Self::Currency), + "unit" => Ok(Self::Unit), + _ => Err(ParseStyleError), + } + } +} + +impl ParsableOptionType for Style {} + +#[derive(Debug, Copy, Clone, Default, Eq, PartialEq)] +pub(crate) enum CurrencyDisplay { + Code, + #[default] + Symbol, + NarrowSymbol, + Name, +} + +impl CurrencyDisplay { + pub(crate) fn to_js_string(self) -> JsString { + match self { + CurrencyDisplay::Code => js_string!("code"), + CurrencyDisplay::Symbol => js_string!("symbol"), + CurrencyDisplay::NarrowSymbol => js_string!("narrowSymbol"), + CurrencyDisplay::Name => js_string!("name"), + } + } +} + +#[derive(Debug)] +pub(crate) struct ParseCurrencyDisplayError; + +impl fmt::Display for ParseCurrencyDisplayError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("provided string was not a valid currencyDisplay option") + } +} + +impl std::str::FromStr for CurrencyDisplay { + type Err = ParseCurrencyDisplayError; + + fn from_str(s: &str) -> Result { + match s { + "code" => Ok(Self::Code), + "symbol" => Ok(Self::Symbol), + "narrowSymbol" => Ok(Self::NarrowSymbol), + "name" => Ok(Self::Name), + _ => Err(ParseCurrencyDisplayError), + } + } +} + +impl ParsableOptionType for CurrencyDisplay {} + +#[derive(Debug, Copy, Clone, Default, Eq, PartialEq)] +pub(crate) enum CurrencySign { + #[default] + Standard, + Accounting, +} + +impl CurrencySign { + pub(crate) fn to_js_string(self) -> JsString { + match self { + CurrencySign::Standard => js_string!("standard"), + CurrencySign::Accounting => js_string!("accounting"), + } + } +} + +#[derive(Debug)] +pub(crate) struct ParseCurrencySignError; + +impl fmt::Display for ParseCurrencySignError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("provided string was not a valid currencySign option") + } +} + +impl std::str::FromStr for CurrencySign { + type Err = ParseCurrencySignError; + + fn from_str(s: &str) -> Result { + match s { + "standard" => Ok(Self::Standard), + "accounting" => Ok(Self::Accounting), + _ => Err(ParseCurrencySignError), + } + } +} + +impl ParsableOptionType for CurrencySign {} + +#[derive(Debug, Copy, Clone, Default, Eq, PartialEq)] +pub(crate) enum UnitDisplay { + #[default] + Short, + Narrow, + Long, +} + +impl UnitDisplay { + pub(crate) fn to_js_string(self) -> JsString { + match self { + UnitDisplay::Short => js_string!("short"), + UnitDisplay::Narrow => js_string!("narrow"), + UnitDisplay::Long => js_string!("long"), + } + } +} + +#[derive(Debug)] +pub(crate) struct ParseUnitDisplayError; + +impl fmt::Display for ParseUnitDisplayError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("provided string was not a valid unitDisplay option") + } +} + +impl std::str::FromStr for UnitDisplay { + type Err = ParseUnitDisplayError; + + fn from_str(s: &str) -> Result { + match s { + "short" => Ok(Self::Short), + "narrow" => Ok(Self::Narrow), + "long" => Ok(Self::Long), + _ => Err(ParseUnitDisplayError), + } + } +} + +impl ParsableOptionType for UnitDisplay {} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub(crate) struct Currency { + // INVARIANT: `inner` must contain only uppercase alphabetic letters. + inner: TinyAsciiStr<3>, +} + +impl Currency { + pub(crate) fn to_js_string(self) -> JsString { + let bytes = self.inner.as_bytes(); + js_string!(&[ + u16::from(bytes[0]), + u16::from(bytes[1]), + u16::from(bytes[2]) + ]) + } +} + +#[derive(Debug)] +pub(crate) struct ParseCurrencyError; + +impl fmt::Display for ParseCurrencyError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("provided string was not a valid currency") + } +} + +impl std::str::FromStr for Currency { + type Err = ParseCurrencyError; + + /// Equivalent to [`IsWellFormedCurrencyCode ( currency )`][spec]. + /// + /// [spec]: https://tc39.es/ecma402/#sec-iswellformedcurrencycode + fn from_str(s: &str) -> Result { + // 1. If the length of currency is not 3, return false. + let bytes = s.as_bytes(); + + if bytes.len() != 3 { + return Err(ParseCurrencyError); + } + + let curr = TinyAsciiStr::from_bytes(bytes).map_err(|_| ParseCurrencyError)?; + + // 2. Let normalized be the ASCII-uppercase of currency. + // 3. If normalized contains any code unit outside of 0x0041 through 0x005A (corresponding + // to Unicode characters LATIN CAPITAL LETTER A through LATIN CAPITAL LETTER Z), return false. + if !curr.is_ascii_alphabetic() { + return Err(ParseCurrencyError); + } + + // 4. Return true. + Ok(Currency { + inner: curr.to_ascii_uppercase(), + }) + } +} + +impl ParsableOptionType for Currency {} + +#[derive(Debug, Eq, PartialEq)] +pub(crate) struct Unit { + // INVARIANT: `numerator` must only contain ASCII lowercase alphabetic letters or `-`. + numerator: &'static str, + // INVARIANT: if `denominator` is not empty, it must only contain ASCII lowercase alphabetic letters or `-` + denominator: &'static str, +} + +impl Unit { + /// Gets the corresponding `JsString` of this unit. + pub(crate) fn to_js_string(&self) -> JsString { + if self.denominator.is_empty() { + js_string!(self.numerator) + } else { + // TODO: this is not optimal for now, but the new JS strings should + // allow us to optimize this to simple casts from ASCII to JsString. + let numerator: Vec = self.numerator.encode_utf16().collect(); + let denominator: Vec = self.denominator.encode_utf16().collect(); + js_string!(&numerator, utf16!("-per-"), &denominator) + } + } +} + +#[derive(Debug)] +pub(crate) struct ParseUnitError; + +impl fmt::Display for ParseUnitError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("provided string was not a valid unit") + } +} + +impl std::str::FromStr for Unit { + type Err = ParseUnitError; + + /// Equivalent to [`IsWellFormedUnitIdentifier ( unitIdentifier )`][spec]. + /// + /// [spec]: https://tc39.es/ecma402/#sec-iswellformedunitidentifier + fn from_str(s: &str) -> Result { + static SANCTIONED_UNITS: [&str; 45] = [ + "acre", + "bit", + "byte", + "celsius", + "centimeter", + "day", + "degree", + "fahrenheit", + "fluid-ounce", + "foot", + "gallon", + "gigabit", + "gigabyte", + "gram", + "hectare", + "hour", + "inch", + "kilobit", + "kilobyte", + "kilogram", + "kilometer", + "liter", + "megabit", + "megabyte", + "meter", + "microsecond", + "mile", + "mile-scandinavian", + "milliliter", + "millimeter", + "millisecond", + "minute", + "month", + "nanosecond", + "ounce", + "percent", + "petabyte", + "pound", + "second", + "stone", + "terabit", + "terabyte", + "week", + "yard", + "year", + ]; + + let (num, den) = s + .split_once("-per-") + .filter(|(_, den)| !den.is_empty()) + .unwrap_or((s, "")); + + let num = SANCTIONED_UNITS + .binary_search(&num) + .map(|i| SANCTIONED_UNITS[i]) + .map_err(|_| ParseUnitError)?; + + let den = if den.is_empty() { + "" + } else { + SANCTIONED_UNITS + .binary_search(&den) + .map(|i| SANCTIONED_UNITS[i]) + .map_err(|_| ParseUnitError)? + }; + + Ok(Self { + numerator: num, + denominator: den, + }) + } +} + +impl ParsableOptionType for Unit {} + +#[derive(Debug)] +#[allow(variant_size_differences)] // 40 bytes is not big enough to require moving `Unit` to the heap. +pub(crate) enum UnitFormatOptions { + Decimal, + Percent, + Currency { + currency: Currency, + display: CurrencyDisplay, + sign: CurrencySign, + }, + Unit { + unit: Unit, + display: UnitDisplay, + }, +} + +impl UnitFormatOptions { + /// Gets the style variant of the `UnitFormatOptions`. + pub(crate) fn style(&self) -> Style { + match self { + Self::Decimal => Style::Decimal, + Self::Percent => Style::Percent, + Self::Currency { .. } => Style::Currency, + Self::Unit { .. } => Style::Unit, + } + } + + /// Abstract operation [`SetNumberFormatUnitOptions ( intlObj, options )`][spec]. + /// + /// [spec]: https://tc39.es/ecma402/#sec-setnumberformatunitoptions + pub(crate) fn from_options(options: &JsObject, context: &mut Context) -> JsResult { + // 1. Let style be ? GetOption(options, "style", string, « "decimal", "percent", "currency", "unit" », "decimal"). + // 2. Set intlObj.[[Style]] to style. + let style: Style = get_option(options, utf16!("style"), context)?.unwrap_or_default(); + + // 3. Let currency be ? GetOption(options, "currency", string, empty, undefined). + // 5. Else, + // a. If IsWellFormedCurrencyCode(currency) is false, throw a RangeError exception. + let currency = get_option(options, utf16!("currency"), context)?; + + // 4. If currency is undefined, then + if currency.is_none() { + // a. If style is "currency", throw a TypeError exception. + if style == Style::Currency { + return Err(JsNativeError::typ() + .with_message( + "cannot format on the currency style without specifying a target currency", + ) + .into()); + } + } + + // 6. Let currencyDisplay be ? GetOption(options, "currencyDisplay", string, « "code", "symbol", "narrowSymbol", "name" », "symbol"). + let currency_display = + get_option(options, utf16!("currencyDisplay"), context)?.unwrap_or_default(); + + // 7. Let currencySign be ? GetOption(options, "currencySign", string, « "standard", "accounting" », "standard"). + let currency_sign = + get_option(options, utf16!("currencySign"), context)?.unwrap_or_default(); + + // 8. Let unit be ? GetOption(options, "unit", string, empty, undefined). + // 10. Else, + // a. If IsWellFormedUnitIdentifier(unit) is false, throw a RangeError exception. + let unit = get_option(options, utf16!("unit"), context)?; + // 9. If unit is undefined, then + if unit.is_none() { + // a. If style is "unit", throw a TypeError exception. + if style == Style::Unit { + return Err(JsNativeError::typ() + .with_message( + "cannot format on the unit style without specifying a target unit", + ) + .into()); + } + } + + // 11. Let unitDisplay be ? GetOption(options, "unitDisplay", string, « "short", "narrow", "long" », "short"). + let unit_display = get_option(options, utf16!("unitDisplay"), context)?.unwrap_or_default(); + + // 14. Return unused. + Ok(match style { + Style::Decimal => UnitFormatOptions::Decimal, + Style::Percent => UnitFormatOptions::Percent, + // 12. If style is "currency", then + Style::Currency => { + UnitFormatOptions::Currency { + // a. Set intlObj.[[Currency]] to the ASCII-uppercase of currency. + currency: currency.expect("asserted above that `currency` is not None"), + // b. Set intlObj.[[CurrencyDisplay]] to currencyDisplay. + display: currency_display, + // c. Set intlObj.[[CurrencySign]] to currencySign. + sign: currency_sign, + } + } + // 13. If style is "unit", then + Style::Unit => { + UnitFormatOptions::Unit { + // a. Set intlObj.[[Unit]] to unit. + unit: unit.expect("asserted above that `unit` is not None"), + // b. Set intlObj.[[UnitDisplay]] to unitDisplay. + display: unit_display, + } + } + }) + } +} #[derive(Debug)] pub(crate) struct DigitFormatOptions { @@ -14,8 +474,544 @@ pub(crate) struct DigitFormatOptions { pub(crate) rounding_priority: RoundingPriority, } +impl DigitFormatOptions { + /// 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 from_options( + options: &JsObject, + min_float_digits_default: u8, + mut max_float_digits_default: u8, + notation: NotationKind, + context: &mut Context, + ) -> JsResult { + // 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 != NotationKind::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(Self { + // 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]. + /// + /// Formats a `FixedDecimal` with the specified digit format options. + /// + /// [spec]: https://tc39.es/ecma402/#sec-formatnumberstring + pub(crate) fn format_fixed_decimal(&self, number: &mut 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), + } + } + + // + fn to_raw_precision( + number: &mut FixedDecimal, + min_precision: u8, + max_precision: u8, + rounding_mode: RoundingMode, + ) -> i16 { + let msb = number.nonzero_magnitude_start(); + let min_msb = msb - i16::from(min_precision) + 1; + let max_msb = msb - i16::from(max_precision) + 1; + round(number, max_msb, rounding_mode, BaseMultiple::MultiplesOf1); + number.trim_end(); + number.pad_end(min_msb); + max_msb + } + + // + 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); + } + + round( + number, + i16::from(rounding_increment.magnitude_offset) - i16::from(max_fraction), + rounding_mode, + rounding_increment.multiple, + ); + number.trim_end(); + number.pad_end(-i16::from(min_fraction)); + -i16::from(max_fraction) + } + + // 3. Let unsignedRoundingMode be GetUnsignedRoundingMode(intlObject.[[RoundingMode]], isNegative). + // Skipping because `FixedDecimal`'s API already provides methods equivalent to `RoundingMode`s. + + match self.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(number, minimum, maximum, self.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( + number, + minimum, + maximum, + self.rounding_increment, + self.rounding_mode, + ); + } + // 6. Else, + RoundingType::MorePrecision { + significant_digits, + fraction_digits, + } + | RoundingType::LessPrecision { + significant_digits, + fraction_digits, + } => { + let prefer_more_precision = + matches!(self.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( + number, + significant_digits.minimum, + significant_digits.maximum, + self.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.minimum, + fraction_digits.maximum, + self.rounding_increment, + self.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 self.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(self.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. + } + + /// 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 format_f64(&self, number: f64) -> FixedDecimal { + // 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"); + + self.format_fixed_decimal(&mut number); + + // 14. Return the Record { [[RoundedNumber]]: x, [[FormattedString]]: string }. + number + } +} + +/// 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, + magnitude_offset: u8, +} + +impl RoundingIncrement { + /// Creates a `RoundingIncrement` from its base multiple (1, 2, 5, or 25) and its + /// exponent (1, 10, 100, or 1000). + #[cfg(test)] + pub(crate) const fn from_parts(multiple: BaseMultiple, exponent: u8) -> Option { + if exponent > 3 { + return None; + } + + Some(Self { + multiple, + magnitude_offset: exponent, + }) + } + + /// Creates a `RoundingIncrement` from the numeric value of the increment. + pub(crate) fn from_u16(increment: u16) -> Option { + 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 + } + } + } +} + #[derive(Debug, Copy, Clone, Default, PartialEq, Eq)] -pub(crate) enum Notation { +pub(crate) enum CompactDisplay { + #[default] + Short, + Long, +} + +impl CompactDisplay { + pub(crate) fn to_js_string(self) -> JsString { + match self { + CompactDisplay::Short => js_string!("short"), + CompactDisplay::Long => js_string!("long"), + } + } +} + +#[derive(Debug)] +pub(crate) struct ParseCompactDisplayError; + +impl fmt::Display for ParseCompactDisplayError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("provided string was not a valid compactDisplay option") + } +} + +impl std::str::FromStr for CompactDisplay { + type Err = ParseCompactDisplayError; + + fn from_str(s: &str) -> Result { + match s { + "short" => Ok(Self::Short), + "long" => Ok(Self::Long), + _ => Err(ParseCompactDisplayError), + } + } +} + +impl ParsableOptionType for CompactDisplay {} + +#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)] +pub(crate) enum NotationKind { #[default] Standard, Scientific, @@ -23,17 +1019,28 @@ pub(crate) enum Notation { Compact, } +impl NotationKind { + pub(crate) fn to_js_string(self) -> JsString { + match self { + NotationKind::Standard => js_string!("standard"), + NotationKind::Scientific => js_string!("scientific"), + NotationKind::Engineering => js_string!("engineering"), + NotationKind::Compact => js_string!("compact"), + } + } +} + #[derive(Debug)] -pub(crate) struct ParseNotationError; +pub(crate) struct ParseNotationKindError; -impl fmt::Display for ParseNotationError { +impl fmt::Display for ParseNotationKindError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("provided string was not a valid notation option") } } -impl std::str::FromStr for Notation { - type Err = ParseNotationError; +impl std::str::FromStr for NotationKind { + type Err = ParseNotationKindError; fn from_str(s: &str) -> Result { match s { @@ -41,12 +1048,31 @@ impl std::str::FromStr for Notation { "scientific" => Ok(Self::Scientific), "engineering" => Ok(Self::Engineering), "compact" => Ok(Self::Compact), - _ => Err(ParseNotationError), + _ => Err(ParseNotationKindError), } } } -impl ParsableOptionType for Notation {} +impl ParsableOptionType for NotationKind {} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub(crate) enum Notation { + Standard, + Scientific, + Engineering, + Compact { display: CompactDisplay }, +} + +impl Notation { + pub(crate) fn kind(self) -> NotationKind { + match self { + Notation::Standard => NotationKind::Standard, + Notation::Scientific => NotationKind::Scientific, + Notation::Engineering => NotationKind::Engineering, + Notation::Compact { .. } => NotationKind::Compact, + } + } +} #[derive(Debug, Copy, Clone, Default, Eq, PartialEq)] pub(crate) enum RoundingPriority { @@ -56,6 +1082,16 @@ pub(crate) enum RoundingPriority { LessPrecision, } +impl RoundingPriority { + pub(crate) fn to_js_string(self) -> JsString { + match self { + RoundingPriority::Auto => js_string!("auto"), + RoundingPriority::MorePrecision => js_string!("morePrecision"), + RoundingPriority::LessPrecision => js_string!("lessPrecision"), + } + } +} + #[derive(Debug)] pub(crate) struct ParseRoundingPriorityError; @@ -80,17 +1116,6 @@ impl std::str::FromStr for RoundingPriority { impl ParsableOptionType for RoundingPriority {} -impl fmt::Display for RoundingPriority { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Auto => "auto", - Self::MorePrecision => "morePrecision", - Self::LessPrecision => "lessPrecision", - } - .fmt(f) - } -} - #[derive(Debug, Copy, Clone, Default, PartialEq, Eq)] pub(crate) enum TrailingZeroDisplay { #[default] @@ -98,6 +1123,15 @@ pub(crate) enum TrailingZeroDisplay { StripIfInteger, } +impl TrailingZeroDisplay { + pub(crate) fn to_js_string(self) -> JsString { + match self { + TrailingZeroDisplay::Auto => js_string!("auto"), + TrailingZeroDisplay::StripIfInteger => js_string!("stripIfInteger"), + } + } +} + #[derive(Debug)] pub(crate) struct ParseTrailingZeroDisplayError; @@ -121,13 +1155,20 @@ impl std::str::FromStr for TrailingZeroDisplay { impl ParsableOptionType for TrailingZeroDisplay {} -impl fmt::Display for TrailingZeroDisplay { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Auto => "auto", - Self::StripIfInteger => "stripIfInteger", +impl OptionType for SignDisplay { + fn from_value(value: crate::JsValue, context: &mut Context) -> JsResult { + match value.to_string(context)?.to_std_string_escaped().as_str() { + "auto" => Ok(Self::Auto), + "never" => Ok(Self::Never), + "always" => Ok(Self::Always), + "exceptZero" => Ok(Self::ExceptZero), + "negative" => Ok(Self::Negative), + _ => Err(JsNativeError::range() + .with_message( + "provided string was not `auto`, `never`, `always`, `exceptZero`, or `negative`", + ) + .into()), } - .fmt(f) } } diff --git a/core/engine/src/builtins/intl/number_format/tests.rs b/core/engine/src/builtins/intl/number_format/tests.rs new file mode 100644 index 0000000000..55f49bba7d --- /dev/null +++ b/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()); + } +} diff --git a/core/engine/src/builtins/intl/number_format/utils.rs b/core/engine/src/builtins/intl/number_format/utils.rs deleted file mode 100644 index ff5f4c265f..0000000000 --- a/core/engine/src/builtins/intl/number_format/utils.rs +++ /dev/null @@ -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 { - 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 { - // 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), - } - } - - // - 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 - } - - // - 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()); - } - } -} diff --git a/core/engine/src/builtins/intl/plural_rules/mod.rs b/core/engine/src/builtins/intl/plural_rules/mod.rs index fa14b352cb..74a07c736e 100644 --- a/core/engine/src/builtins/intl/plural_rules/mod.rs +++ b/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). diff --git a/core/engine/src/builtins/mod.rs b/core/engine/src/builtins/mod.rs index d20452a5f9..0cbe765894 100644 --- a/core/engine/src/builtins/mod.rs +++ b/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")] diff --git a/core/engine/src/builtins/options.rs b/core/engine/src/builtins/options.rs index f25899a240..cae5b7f483 100644 --- a/core/engine/src/builtins/options.rs +++ b/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)] diff --git a/core/engine/src/context/intrinsics.rs b/core/engine/src/context/intrinsics.rs index 19f5edc65b..b1056f71bb 100644 --- a/core/engine/src/context/intrinsics.rs +++ b/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 { 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, /// [`%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 { + 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 { self.intl.clone() } diff --git a/core/engine/src/context/mod.rs b/core/engine/src/context/mod.rs index 3f5daa086a..baff917997 100644 --- a/core/engine/src/context/mod.rs +++ b/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 { - 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 = if let Some(loader) = self.module_loader { diff --git a/core/engine/src/object/jsobject.rs b/core/engine/src/object/jsobject.rs index fbc517cfde..0303f6076f 100644 --- a/core/engine/src/object/jsobject.rs +++ b/core/engine/src/object/jsobject.rs @@ -597,6 +597,28 @@ impl JsObject { 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>>(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] diff --git a/core/engine/src/object/operations.rs b/core/engine/src/object/operations.rs index b2674f1e17..9d4ead2165 100644 --- a/core/engine/src/object/operations.rs +++ b/core/engine/src/object/operations.rs @@ -1302,11 +1302,7 @@ impl JsValue { if let Some(bound_function) = function.downcast_ref::() { // 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 { diff --git a/core/engine/src/realm.rs b/core/engine/src/realm.rs index 69c9f15f02..bbf3dce18b 100644 --- a/core/engine/src/realm.rs +++ b/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 { 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`. diff --git a/core/engine/src/string/common.rs b/core/engine/src/string/common.rs index 275f6ed25f..aeda336c4b 100644 --- a/core/engine/src/string/common.rs +++ b/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"), diff --git a/test262_config.toml b/test262_config.toml index a435c5294d..d2b721243f 100644 --- a/test262_config.toml +++ b/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 = []