From 3bc6a462e5a30bd41ffc37cc63b8da4943513c9a Mon Sep 17 00:00:00 2001 From: jedel1043 Date: Sun, 24 Sep 2023 05:13:14 -0600 Subject: [PATCH 1/5] Implement `PluralRules` --- Cargo.lock | 6 +- boa_engine/Cargo.toml | 2 + .../src/builtins/intl/collator/options.rs | 20 +- boa_engine/src/builtins/intl/mod.rs | 9 +- .../src/builtins/intl/number_format/mod.rs | 4 + .../builtins/intl/number_format/options.rs | 241 +++++++++++ .../src/builtins/intl/number_format/utils.rs | 405 +++++++++++++++++ boa_engine/src/builtins/intl/options.rs | 50 +-- .../src/builtins/intl/plural_rules/mod.rs | 409 ++++++++++++++++++ .../src/builtins/intl/plural_rules/options.rs | 15 + boa_engine/src/builtins/intl/segmenter/mod.rs | 4 +- boa_engine/src/builtins/mod.rs | 1 + boa_engine/src/context/icu.rs | 16 + boa_engine/src/context/intrinsics.rs | 17 + boa_engine/src/object/mod.rs | 49 ++- test262_config.toml | 4 +- 16 files changed, 1211 insertions(+), 41 deletions(-) create mode 100644 boa_engine/src/builtins/intl/number_format/mod.rs create mode 100644 boa_engine/src/builtins/intl/number_format/options.rs create mode 100644 boa_engine/src/builtins/intl/number_format/utils.rs create mode 100644 boa_engine/src/builtins/intl/plural_rules/mod.rs create mode 100644 boa_engine/src/builtins/intl/plural_rules/options.rs diff --git a/Cargo.lock b/Cargo.lock index d106db447a3..91ec1c5235e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -418,6 +418,7 @@ dependencies = [ "criterion", "dashmap", "fast-float", + "fixed_decimal", "float-cmp", "futures-lite", "icu_calendar", @@ -1413,11 +1414,12 @@ dependencies = [ [[package]] name = "fixed_decimal" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c9eab2dd2aadbc55056ed228ccc4be42d07cd61aee72d48768f8ac2e4ab7d54" +checksum = "5287d527037d0f35c8801880361eb38bb9bce194805350052c2a79538388faeb" dependencies = [ "displaydoc", + "ryu", "smallvec", "writeable", ] diff --git a/boa_engine/Cargo.toml b/boa_engine/Cargo.toml index 75c2e796f83..a861f98c42a 100644 --- a/boa_engine/Cargo.toml +++ b/boa_engine/Cargo.toml @@ -32,6 +32,7 @@ intl = [ "dep:sys-locale", "dep:yoke", "dep:zerofrom", + "dep:fixed_decimal", ] fuzz = ["boa_ast/arbitrary", "boa_interner/arbitrary"] @@ -93,6 +94,7 @@ writeable = { version = "0.5.2", optional = true } yoke = { version = "0.7.1", optional = true } zerofrom = { version = "0.1.2", optional = true } sys-locale = { version = "0.3.1", optional = true } +fixed_decimal = { version = "0.5.4", features = ["ryu"], optional = true} [dev-dependencies] criterion = "0.5.1" diff --git a/boa_engine/src/builtins/intl/collator/options.rs b/boa_engine/src/builtins/intl/collator/options.rs index a2c9e5a540d..51a6c6bdeb5 100644 --- a/boa_engine/src/builtins/intl/collator/options.rs +++ b/boa_engine/src/builtins/intl/collator/options.rs @@ -1,8 +1,11 @@ use std::str::FromStr; -use icu_collator::{CaseLevel, Strength}; +use icu_collator::{CaseFirst, CaseLevel, Strength}; -use crate::builtins::intl::options::OptionTypeParsable; +use crate::{ + builtins::intl::options::{OptionType, OptionTypeParsable}, + Context, JsNativeError, JsResult, JsValue, +}; #[derive(Debug, Clone, Copy)] pub(crate) enum Sensitivity { @@ -78,3 +81,16 @@ impl FromStr for Usage { } impl OptionTypeParsable for Usage {} + +impl OptionType for CaseFirst { + fn from_value(value: JsValue, context: &mut Context<'_>) -> JsResult { + match value.to_string(context)?.to_std_string_escaped().as_str() { + "upper" => Ok(Self::UpperFirst), + "lower" => Ok(Self::LowerFirst), + "false" => Ok(Self::Off), + _ => Err(JsNativeError::range() + .with_message("provided string was not `upper`, `lower` or `false`") + .into()), + } + } +} diff --git a/boa_engine/src/builtins/intl/mod.rs b/boa_engine/src/builtins/intl/mod.rs index 1cdabf97eb7..fb0dac97912 100644 --- a/boa_engine/src/builtins/intl/mod.rs +++ b/boa_engine/src/builtins/intl/mod.rs @@ -26,11 +26,13 @@ pub(crate) mod collator; pub(crate) mod date_time_format; pub(crate) mod list_format; pub(crate) mod locale; +pub(crate) mod number_format; +pub(crate) mod plural_rules; pub(crate) mod segmenter; pub(crate) use self::{ collator::Collator, date_time_format::DateTimeFormat, list_format::ListFormat, locale::Locale, - segmenter::Segmenter, + segmenter::Segmenter, plural_rules::PluralRules }; mod options; @@ -73,6 +75,11 @@ impl IntrinsicObject for Intl { realm.intrinsics().constructors().segmenter().constructor(), Segmenter::ATTRIBUTE, ) + .static_property( + PluralRules::NAME, + realm.intrinsics().constructors().plural_rules().constructor(), + PluralRules::ATTRIBUTE, + ) .static_property( DateTimeFormat::NAME, realm diff --git a/boa_engine/src/builtins/intl/number_format/mod.rs b/boa_engine/src/builtins/intl/number_format/mod.rs new file mode 100644 index 00000000000..81f115781c5 --- /dev/null +++ b/boa_engine/src/builtins/intl/number_format/mod.rs @@ -0,0 +1,4 @@ +mod options; +mod utils; +pub(crate) use options::*; +pub(crate) use utils::*; diff --git a/boa_engine/src/builtins/intl/number_format/options.rs b/boa_engine/src/builtins/intl/number_format/options.rs new file mode 100644 index 00000000000..1941df10138 --- /dev/null +++ b/boa_engine/src/builtins/intl/number_format/options.rs @@ -0,0 +1,241 @@ +use std::fmt; + +use crate::builtins::intl::options::OptionTypeParsable; + +#[derive(Debug)] +pub(crate) struct DigitFormatOptions { + pub(crate) minimum_integer_digits: u8, + pub(crate) rounding_increment: u16, + pub(crate) rounding_mode: RoundingMode, + pub(crate) trailing_zero_display: TrailingZeroDisplay, + pub(crate) rounding_type: RoundingType, + pub(crate) rounding_priority: RoundingPriority, +} + +#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)] +pub(crate) enum Notation { + #[default] + Standard, + Scientific, + Engineering, + Compact, +} + +#[derive(Debug)] +pub(crate) struct ParseNotationError; + +impl fmt::Display for ParseNotationError { + 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; + + fn from_str(s: &str) -> Result { + match s { + "standard" => Ok(Self::Standard), + "scientific" => Ok(Self::Scientific), + "engineering" => Ok(Self::Engineering), + "compact" => Ok(Self::Compact), + _ => Err(ParseNotationError), + } + } +} + +impl OptionTypeParsable for Notation {} + +#[derive(Debug, Copy, Clone, Default, Eq, PartialEq)] +pub(crate) enum RoundingPriority { + #[default] + Auto, + MorePrecision, + LessPrecision, +} + +#[derive(Debug)] +pub(crate) struct ParseRoundingPriorityError; + +impl fmt::Display for ParseRoundingPriorityError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("provided string was not a valid rounding priority") + } +} + +impl std::str::FromStr for RoundingPriority { + type Err = ParseRoundingPriorityError; + + fn from_str(s: &str) -> Result { + match s { + "auto" => Ok(Self::Auto), + "morePrecision" => Ok(Self::MorePrecision), + "lessPrecision" => Ok(Self::LessPrecision), + _ => Err(ParseRoundingPriorityError), + } + } +} + +impl OptionTypeParsable 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)] +pub(crate) enum RoundingMode { + Ceil, + Floor, + Expand, + Trunc, + HalfCeil, + HalfFloor, + #[default] + HalfExpand, + HalfTrunc, + HalfEven, +} + +#[derive(Debug)] +pub(crate) struct ParseRoundingModeError; + +impl fmt::Display for ParseRoundingModeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("provided string was not a valid rounding mode") + } +} + +impl std::str::FromStr for RoundingMode { + type Err = ParseRoundingModeError; + + fn from_str(s: &str) -> Result { + match s { + "ceil" => Ok(Self::Ceil), + "floor" => Ok(Self::Floor), + "expand" => Ok(Self::Expand), + "trunc" => Ok(Self::Trunc), + "halfCeil" => Ok(Self::HalfCeil), + "halfFloor" => Ok(Self::HalfFloor), + "halfExpand" => Ok(Self::HalfExpand), + "halfTrunc" => Ok(Self::HalfTrunc), + "halfEven" => Ok(Self::HalfEven), + _ => Err(ParseRoundingModeError), + } + } +} + +impl OptionTypeParsable 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) + } +} + +#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)] +pub(crate) enum TrailingZeroDisplay { + #[default] + Auto, + StripIfInteger, +} + +#[derive(Debug)] +pub(crate) struct ParseTrailingZeroDisplayError; + +impl fmt::Display for ParseTrailingZeroDisplayError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("provided string was not a valid trailing zero display option") + } +} + +impl std::str::FromStr for TrailingZeroDisplay { + type Err = ParseTrailingZeroDisplayError; + + fn from_str(s: &str) -> Result { + match s { + "auto" => Ok(Self::Auto), + "stripIfInteger" => Ok(Self::StripIfInteger), + _ => Err(ParseTrailingZeroDisplayError), + } + } +} + +impl OptionTypeParsable 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", + } + .fmt(f) + } +} + +#[derive(Debug, Copy, Clone)] +pub(crate) struct Extrema { + pub(crate) minimum: T, + pub(crate) maximum: T, +} + +#[derive(Debug, Copy, Clone)] +pub(crate) enum RoundingType { + MorePrecision { + significant_digits: Extrema, + fraction_digits: Extrema, + }, + LessPrecision { + significant_digits: Extrema, + fraction_digits: Extrema, + }, + SignificantDigits(Extrema), + FractionDigits(Extrema), +} + +impl RoundingType { + /// Gets the significant digit limits of the rounding type, or `None` otherwise. + pub(crate) const fn significant_digits(self) -> Option> { + match self { + Self::MorePrecision { + significant_digits, .. + } + | Self::LessPrecision { + significant_digits, .. + } + | Self::SignificantDigits(significant_digits) => Some(significant_digits), + Self::FractionDigits(_) => None, + } + } + + /// Gets the fraction digit limits of the rounding type, or `None` otherwise. + pub(crate) const fn fraction_digits(self) -> Option> { + match self { + Self::MorePrecision { + fraction_digits, .. + } + | Self::LessPrecision { + fraction_digits, .. + } + | Self::FractionDigits(fraction_digits) => Some(fraction_digits), + Self::SignificantDigits(_) => None, + } + } +} diff --git a/boa_engine/src/builtins/intl/number_format/utils.rs b/boa_engine/src/builtins/intl/number_format/utils.rs new file mode 100644 index 00000000000..b5ba7fb3e9a --- /dev/null +++ b/boa_engine/src/builtins/intl/number_format/utils.rs @@ -0,0 +1,405 @@ +use boa_macros::utf16; +use fixed_decimal::{FixedDecimal, FloatPrecision}; + +use crate::{ + builtins::intl::{ + number_format::{Extrema, RoundingMode, RoundingType, TrailingZeroDisplay}, + options::{default_number_option, get_number_option, get_option}, + }, + Context, JsNativeError, JsObject, JsResult, +}; + +use super::{DigitFormatOptions, Notation, RoundingPriority}; + +/// 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 { + const VALID_ROUNDING_INCREMENTS: [u16; 15] = [ + 1, 2, 5, 10, 20, 25, 50, 100, 200, 250, 500, 1000, 2000, 2500, 5000, + ]; + + // 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"), false, 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); + + if !VALID_ROUNDING_INCREMENTS.contains(&rounding_increment) { + return Err(JsNativeError::range() + .with_message("invalid value for option `roundingIncrement`") + .into()); + } + + // 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"), false, context)?.unwrap_or_default(); + + // 11. Let trailingZeroDisplay be ? GetOption(options, "trailingZeroDisplay", string, « "auto", "stripIfInteger" », "auto"). + let trailing_zero_display = + get_option(options, utf16!("trailingZeroDisplay"), false, 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 != 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 != 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) { + match mode { + RoundingMode::Ceil => number.ceil(position), + RoundingMode::Floor => number.floor(position), + RoundingMode::Expand => number.expand(position), + RoundingMode::Trunc => number.trunc(position), + RoundingMode::HalfCeil => number.half_ceil(position), + RoundingMode::HalfFloor => number.half_floor(position), + RoundingMode::HalfExpand => number.half_expand(position), + RoundingMode::HalfTrunc => number.half_trunc(position), + RoundingMode::HalfEven => number.half_even(position), + } + } + + // + 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); + max_msb + } + + // + fn to_raw_fixed( + number: &mut FixedDecimal, + min_fraction: u8, + max_fraction: u8, + // TODO: missing support for `roundingIncrement` on `FixedDecimal`. + _rounding_increment: u16, + rounding_mode: RoundingMode, + ) -> i16 { + number.pad_end(-i16::from(min_fraction)); + round(number, -i16::from(max_fraction), rounding_mode); + -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 +} diff --git a/boa_engine/src/builtins/intl/options.rs b/boa_engine/src/builtins/intl/options.rs index 603a81722e7..0a6ce6c2675 100644 --- a/boa_engine/src/builtins/intl/options.rs +++ b/boa_engine/src/builtins/intl/options.rs @@ -1,6 +1,6 @@ use std::{fmt::Display, str::FromStr}; -use icu_collator::CaseFirst; +use num_traits::FromPrimitive; use crate::{ object::{JsObject, ObjectData}, @@ -91,19 +91,6 @@ impl FromStr for LocaleMatcher { impl OptionTypeParsable for LocaleMatcher {} -impl OptionType for CaseFirst { - fn from_value(value: JsValue, context: &mut Context<'_>) -> JsResult { - match value.to_string(context)?.to_std_string_escaped().as_str() { - "upper" => Ok(Self::UpperFirst), - "lower" => Ok(Self::LowerFirst), - "false" => Ok(Self::Off), - _ => Err(JsNativeError::range() - .with_message("provided string was not `upper`, `lower` or `false`") - .into()), - } - } -} - /// Abstract operation [`GetOption ( options, property, type, values, fallback )`][spec] /// /// Extracts the value of the property named `property` from the provided `options` object, @@ -154,21 +141,22 @@ pub(super) fn get_option( /// - [ECMAScript reference][spec] /// /// [spec]: https://tc39.es/ecma402/#sec-getnumberoption -#[allow(unused)] -pub(super) fn get_number_option( +pub(super) fn get_number_option( options: &JsObject, property: &[u16], - minimum: f64, - maximum: f64, - fallback: Option, + minimum: T, + maximum: T, context: &mut Context<'_>, -) -> JsResult> { +) -> JsResult> +where + T: Into + FromPrimitive, +{ // 1. Assert: Type(options) is Object. // 2. Let value be ? Get(options, property). let value = options.get(property, context)?; // 3. Return ? DefaultNumberOption(value, minimum, maximum, fallback). - default_number_option(&value, minimum, maximum, fallback, context) + default_number_option(&value, minimum, maximum, context) } /// Abstract operation [`DefaultNumberOption ( value, minimum, maximum, fallback )`][spec] @@ -177,31 +165,33 @@ pub(super) fn get_number_option( /// and fills in a `fallback` value if necessary. /// /// [spec]: https://tc39.es/ecma402/#sec-defaultnumberoption -#[allow(unused)] -pub(super) fn default_number_option( +pub(super) fn default_number_option( value: &JsValue, - minimum: f64, - maximum: f64, - fallback: Option, + minimum: T, + maximum: T, context: &mut Context<'_>, -) -> JsResult> { +) -> JsResult> +where + T: Into + FromPrimitive, +{ // 1. If value is undefined, return fallback. if value.is_undefined() { - return Ok(fallback); + return Ok(None); } // 2. Set value to ? ToNumber(value). let value = value.to_number(context)?; // 3. If value is NaN or less than minimum or greater than maximum, throw a RangeError exception. - if value.is_nan() || value < minimum || value > maximum { + if value.is_nan() || value < minimum.into() || value > maximum.into() { return Err(JsNativeError::range() .with_message("DefaultNumberOption: value is out of range.") .into()); } // 4. Return floor(value). - Ok(Some(value.floor())) + // We already asserted the range of `value` with the conditional above. + Ok(T::from_f64(value)) } /// Abstract operation [`GetOptionsObject ( options )`][spec] diff --git a/boa_engine/src/builtins/intl/plural_rules/mod.rs b/boa_engine/src/builtins/intl/plural_rules/mod.rs new file mode 100644 index 00000000000..88a06d6c6ae --- /dev/null +++ b/boa_engine/src/builtins/intl/plural_rules/mod.rs @@ -0,0 +1,409 @@ +mod options; + +use boa_macros::utf16; +use boa_profiler::Profiler; +use fixed_decimal::FixedDecimal; +use icu_locid::Locale; +use icu_plurals::{ + provider::CardinalV1Marker, PluralCategory, PluralRuleType, PluralRules as NativePluralRules, +}; +use icu_provider::DataLocale; + +use crate::{ + builtins::{Array, BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject}, + context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, + js_string, + object::{internal_methods::get_prototype_from_constructor, ObjectData, ObjectInitializer}, + property::Attribute, + realm::Realm, + Context, JsArgs, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, +}; + +use super::{ + locale::{canonicalize_locale_list, resolve_locale, supported_locales}, + number_format::{ + f64_to_formatted_fixed_decimal, get_digit_format_options, DigitFormatOptions, Extrema, + Notation, + }, + options::{coerce_options_to_object, get_option, IntlOptions, LocaleMatcher}, + Service, +}; + +#[derive(Debug)] +pub struct PluralRules { + locale: Locale, + native: NativePluralRules, + rule_type: PluralRuleType, + format_options: DigitFormatOptions, +} + +impl Service for PluralRules { + type LangMarker = CardinalV1Marker; + + type LocaleOptions = (); +} + +impl IntrinsicObject for PluralRules { + fn init(realm: &Realm) { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + BuiltInBuilder::from_standard_constructor::(realm) + .static_method(Self::supported_locales_of, "supportedLocalesOf", 1) + .property( + JsSymbol::to_string_tag(), + "Intl.PluralRules", + Attribute::CONFIGURABLE, + ) + .method(Self::resolved_options, "resolvedOptions", 0) + .method(Self::select, "select", 1) + .build(); + } + + fn get(intrinsics: &Intrinsics) -> JsObject { + Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor() + } +} + +impl BuiltInObject for PluralRules { + const NAME: &'static str = "PluralRules"; +} + +impl BuiltInConstructor for PluralRules { + const LENGTH: usize = 0; + + const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor = + StandardConstructors::plural_rules; + + fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + // 1. If NewTarget is undefined, throw a TypeError exception. + if new_target.is_undefined() { + return Err(JsNativeError::typ() + .with_message("cannot call `Intl.PluralRules` constructor without `new`") + .into()); + } + // 2. Let pluralRules be ? OrdinaryCreateFromConstructor(NewTarget, "%PluralRules.prototype%", + // « [[InitializedPluralRules]], [[Locale]], [[Type]], [[MinimumIntegerDigits]], + // [[MinimumFractionDigits]], [[MaximumFractionDigits]], [[MinimumSignificantDigits]], + // [[MaximumSignificantDigits]], [[RoundingType]], [[RoundingIncrement]], [[RoundingMode]], + // [[ComputedRoundingPriority]], [[TrailingZeroDisplay]] »). + // 3. Return ? InitializePluralRules(pluralRules, locales, options). + + // + + let locales = args.get_or_undefined(0); + let options = args.get_or_undefined(1); + + // 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"), false, context)? + .unwrap_or_default(); + + // 6. Let t be ? GetOption(options, "type", string, « "cardinal", "ordinal" », "cardinal"). + // 7. Set pluralRules.[[Type]] to t. + let rule_type = get_option::(&options, utf16!("type"), false, 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)?; + + // 9. Let localeData be %PluralRules%.[[LocaleData]]. + // 10. Let r be ResolveLocale(%PluralRules%.[[AvailableLocales]], requestedLocales, opt, %PluralRules%.[[RelevantExtensionKeys]], localeData). + // 11. Set pluralRules.[[Locale]] to r.[[locale]]. + let locale = resolve_locale::( + &requested_locales, + &mut IntlOptions { + matcher, + ..Default::default() + }, + context.icu(), + ); + + let native = context + .icu() + .provider() + .try_new_plural_rules(&DataLocale::from(&locale), rule_type) + .map_err(|err| JsNativeError::typ().with_message(err.to_string()))?; + + let proto = get_prototype_from_constructor( + new_target, + StandardConstructors::plural_rules, + context, + )?; + + // 12. Return pluralRules. + Ok(JsObject::from_proto_and_data_with_shared_shape( + context.root_shape(), + proto, + ObjectData::plural_rules(Self { + locale, + native, + rule_type, + format_options, + }), + ) + .into()) + } +} + +impl PluralRules { + /// [`Intl.PluralRules.prototype.select ( value )`][spec]. + /// + /// Returns a string indicating which plural rule to use for locale-aware formatting of a number. + /// + /// More information: + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma402/#sec-intl.pluralrules.prototype.select + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/select + fn select(this: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResult { + // 1. Let pr be the this value. + // 2. Perform ? RequireInternalSlot(pr, [[InitializedPluralRules]]). + let plural_rules = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message( + "`resolved_options` can only be called on an `Intl.PluralRules` object", + ) + })?; + let plural_rules = plural_rules.as_plural_rules().ok_or_else(|| { + JsNativeError::typ().with_message( + "`resolved_options` can only be called on an `Intl.PluralRules` object", + ) + })?; + + let n = args.get_or_undefined(0).to_number(context)?; + + Ok(plural_category_to_js_string(resolve_plural(plural_rules, n).category).into()) + } + /// [`Intl.PluralRules.supportedLocalesOf ( locales [ , options ] )`][spec]. + /// + /// Returns an array containing those of the provided locales that are supported in plural rules + /// without having to fall back to the runtime's default locale. + /// + /// More information: + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma402/#sec-intl.pluralrules.supportedlocalesof + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/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 %PluralRules%.[[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) + } + + /// [`Intl.PluralRules.prototype.resolvedOptions ( )`][spec]. + /// + /// Returns a new object with properties reflecting the locale and options computed during the + /// construction of the current `Intl.PluralRules` object. + /// + /// More information: + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma402/#sec-intl.pluralrules.prototype.resolvedoptions + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/resolvedOptions + fn resolved_options( + this: &JsValue, + _: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + // 1. Let pr be the this value. + // 2. Perform ? RequireInternalSlot(pr, [[InitializedPluralRules]]). + let plural_rules = this.as_object().map(JsObject::borrow).ok_or_else(|| { + JsNativeError::typ().with_message( + "`resolved_options` can only be called on an `Intl.PluralRules` object", + ) + })?; + let plural_rules = plural_rules.as_plural_rules().ok_or_else(|| { + JsNativeError::typ().with_message( + "`resolved_options` can only be called on an `Intl.PluralRules` object", + ) + })?; + + // 3. Let options be OrdinaryObjectCreate(%Object.prototype%). + // 4. For each row of Table 16, 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 pr's internal slot whose name is the Internal Slot value of the current row. + // c. If v is not undefined, then + // i. Perform ! CreateDataPropertyOrThrow(options, p, v). + let mut options = ObjectInitializer::new(context); + options + .property( + js_string!("locale"), + plural_rules.locale.to_string(), + Attribute::all(), + ) + .property( + js_string!("type"), + match plural_rules.rule_type { + PluralRuleType::Cardinal => "cardinal", + PluralRuleType::Ordinal => "ordinal", + _ => "unknown", + }, + Attribute::all(), + ) + .property( + js_string!("minimumIntegerDigits"), + plural_rules.format_options.minimum_integer_digits, + Attribute::all(), + ); + + if let Some(Extrema { minimum, maximum }) = + plural_rules.format_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 }) = plural_rules + .format_options + .rounding_type + .significant_digits() + { + options + .property( + js_string!("minimumSignificantDigits"), + minimum, + Attribute::all(), + ) + .property( + js_string!("maximumSignificantDigits"), + maximum, + Attribute::all(), + ); + } + + options + .property( + js_string!("roundingMode"), + plural_rules.format_options.rounding_mode.to_string(), + Attribute::all(), + ) + .property( + js_string!("roundingIncrement"), + plural_rules.format_options.rounding_increment, + Attribute::all(), + ) + .property( + js_string!("trailingZeroDisplay"), + plural_rules + .format_options + .trailing_zero_display + .to_string(), + Attribute::all(), + ); + + // 5. Let pluralCategories be a List of Strings containing all possible results of PluralRuleSelect + // for the selected locale pr.[[Locale]]. + let plural_categories = Array::create_array_from_list( + plural_rules + .native + .categories() + .map(|category| plural_category_to_js_string(category).into()), + options.context(), + ); + + // 6. Perform ! CreateDataProperty(options, "pluralCategories", CreateArrayFromList(pluralCategories)). + options.property( + js_string!("pluralCategories"), + plural_categories, + Attribute::all(), + ); + + // 7. If pr.[[RoundingType]] is morePrecision, then + // a. Perform ! CreateDataPropertyOrThrow(options, "roundingPriority", "morePrecision"). + // 8. Else if pr.[[RoundingType]] is lessPrecision, then + // a. Perform ! CreateDataPropertyOrThrow(options, "roundingPriority", "lessPrecision"). + // 9. Else, + // a. Perform ! CreateDataPropertyOrThrow(options, "roundingPriority", "auto"). + options.property( + js_string!("roundingPriority"), + plural_rules.format_options.rounding_priority.to_string(), + Attribute::all(), + ); + + // 10. Return options. + Ok(options.build().into()) + } +} + +#[derive(Debug)] +#[allow(unused)] // Will be used when we implement `selectRange` +struct ResolvedPlural { + category: PluralCategory, + formatted: Option, +} + +/// Abstract operation [`ResolvePlural ( pluralRules, n )`][spec] +/// +/// Gets the plural corresponding to the number with the provided formatting options. +/// +/// [spec]: https://tc39.es/ecma402/#sec-resolveplural +fn resolve_plural(plural_rules: &PluralRules, n: f64) -> ResolvedPlural { + // 1. Assert: Type(pluralRules) is Object. + // 2. Assert: pluralRules has an [[InitializedPluralRules]] internal slot. + // 3. Assert: Type(n) is Number. + // 4. If n is not a finite Number, then + if !n.is_finite() { + // a. Return "other". + return ResolvedPlural { + category: PluralCategory::Other, + formatted: None, + }; + } + + // 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); + + // 8. Let s be res.[[FormattedString]]. + // 9. Let operands be ! GetOperands(s). + // 10. Let p be ! PluralRuleSelect(locale, type, n, operands). + let category = plural_rules.native.category_for(&fixed); + + // 11. Return the Record { [[PluralCategory]]: p, [[FormattedString]]: s }. + ResolvedPlural { + category, + formatted: Some(fixed), + } +} + +fn plural_category_to_js_string(category: PluralCategory) -> JsString { + match category { + PluralCategory::Zero => js_string!("zero"), + PluralCategory::One => js_string!("one"), + PluralCategory::Two => js_string!("two"), + PluralCategory::Few => js_string!("few"), + PluralCategory::Many => js_string!("many"), + PluralCategory::Other => js_string!("other"), + } +} diff --git a/boa_engine/src/builtins/intl/plural_rules/options.rs b/boa_engine/src/builtins/intl/plural_rules/options.rs new file mode 100644 index 00000000000..f70f247bef6 --- /dev/null +++ b/boa_engine/src/builtins/intl/plural_rules/options.rs @@ -0,0 +1,15 @@ +use icu_plurals::PluralRuleType; + +use crate::{builtins::intl::options::OptionType, Context, JsNativeError, JsResult, JsValue}; + +impl OptionType for PluralRuleType { + fn from_value(value: JsValue, context: &mut Context<'_>) -> JsResult { + match value.to_string(context)?.to_std_string_escaped().as_str() { + "cardinal" => Ok(Self::Cardinal), + "ordinal" => Ok(Self::Ordinal), + _ => Err(JsNativeError::range() + .with_message("provided string was not `cardinal` or `ordinal`") + .into()), + } + } +} diff --git a/boa_engine/src/builtins/intl/segmenter/mod.rs b/boa_engine/src/builtins/intl/segmenter/mod.rs index 2cdd17ac2d5..258561f868f 100644 --- a/boa_engine/src/builtins/intl/segmenter/mod.rs +++ b/boa_engine/src/builtins/intl/segmenter/mod.rs @@ -177,8 +177,8 @@ impl BuiltInConstructor for Segmenter { impl Segmenter { /// [`Intl.Segmenter.supportedLocalesOf ( locales [ , options ] )`][spec]. /// - /// Returns an array containing those of the provided locales that are supported in list - /// formatting without having to fall back to the runtime's default locale. + /// Returns an array containing those of the provided locales that are supported in segmenting + /// without having to fall back to the runtime's default locale. /// /// More information: /// - [MDN documentation][mdn] diff --git a/boa_engine/src/builtins/mod.rs b/boa_engine/src/builtins/mod.rs index 798f8d70b8e..02af65ee837 100644 --- a/boa_engine/src/builtins/mod.rs +++ b/boa_engine/src/builtins/mod.rs @@ -269,6 +269,7 @@ impl Realm { intl::Segmenter::init(self); intl::segmenter::Segments::init(self); intl::segmenter::SegmentIterator::init(self); + intl::PluralRules::init(self); } } } diff --git a/boa_engine/src/context/icu.rs b/boa_engine/src/context/icu.rs index ab4bd38633a..b6416ec75c5 100644 --- a/boa_engine/src/context/icu.rs +++ b/boa_engine/src/context/icu.rs @@ -4,6 +4,7 @@ use icu_collator::{Collator, CollatorError, CollatorOptions}; use icu_list::{ListError, ListFormatter, ListLength}; use icu_locid_transform::{LocaleCanonicalizer, LocaleExpander, LocaleTransformError}; use icu_normalizer::{ComposingNormalizer, DecomposingNormalizer, NormalizerError}; +use icu_plurals::{PluralRuleType, PluralRules, PluralsError}; use icu_provider::{ AnyProvider, AsDeserializingBufferProvider, AsDowncastingAnyProvider, BufferProvider, DataError, DataLocale, DataProvider, DataRequest, DataResponse, KeyedDataMarker, MaybeSendSync, @@ -153,6 +154,7 @@ impl BoaProvider<'_> { } } + /// Creates a [`StringNormalizers`] from the provided [`DataProvider`]. pub(crate) fn try_new_string_normalizers(&self) -> Result { Ok(match *self { BoaProvider::Buffer(buf) => StringNormalizers { @@ -169,6 +171,20 @@ impl BoaProvider<'_> { }, }) } + + /// Creates a [`PluralRules`] from the provided [`DataProvider`] and options. + pub(crate) fn try_new_plural_rules( + &self, + locale: &DataLocale, + rule_type: PluralRuleType, + ) -> Result { + match *self { + BoaProvider::Buffer(buf) => { + PluralRules::try_new_with_buffer_provider(buf, locale, rule_type) + } + BoaProvider::Any(any) => PluralRules::try_new_with_any_provider(any, locale, rule_type), + } + } } /// Error thrown when the engine cannot initialize the ICU tools from a data provider. diff --git a/boa_engine/src/context/intrinsics.rs b/boa_engine/src/context/intrinsics.rs index 92734cc0bde..ef06b3e3479 100644 --- a/boa_engine/src/context/intrinsics.rs +++ b/boa_engine/src/context/intrinsics.rs @@ -155,6 +155,8 @@ pub struct StandardConstructors { locale: StandardConstructor, #[cfg(feature = "intl")] segmenter: StandardConstructor, + #[cfg(feature = "intl")] + plural_rules: StandardConstructor, } impl Default for StandardConstructors { @@ -229,6 +231,8 @@ impl Default for StandardConstructors { locale: StandardConstructor::default(), #[cfg(feature = "intl")] segmenter: StandardConstructor::default(), + #[cfg(feature = "intl")] + plural_rules: StandardConstructor::default(), } } } @@ -801,6 +805,19 @@ impl StandardConstructors { pub const fn segmenter(&self) -> &StandardConstructor { &self.segmenter } + + /// Returns the `Intl.PluralRules` constructor. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma402/#sec-intl.pluralrules + #[inline] + #[must_use] + #[cfg(feature = "intl")] + pub const fn plural_rules(&self) -> &StandardConstructor { + &self.plural_rules + } } /// Cached intrinsic objects diff --git a/boa_engine/src/object/mod.rs b/boa_engine/src/object/mod.rs index a7e4db77fad..bdf9de6a73e 100644 --- a/boa_engine/src/object/mod.rs +++ b/boa_engine/src/object/mod.rs @@ -32,6 +32,7 @@ use crate::builtins::intl::{ collator::Collator, date_time_format::DateTimeFormat, list_format::ListFormat, + plural_rules::PluralRules, segmenter::{SegmentIterator, Segmenter, Segments}, }; use crate::{ @@ -356,6 +357,10 @@ pub enum ObjectKind { /// The `Segment Iterator` object kind. #[cfg(feature = "intl")] SegmentIterator(SegmentIterator), + + /// The `PluralRules` object kind. + #[cfg(feature = "intl")] + PluralRules(PluralRules), } unsafe impl Trace for ObjectKind { @@ -394,7 +399,10 @@ unsafe impl Trace for ObjectKind { #[cfg(feature = "intl")] Self::SegmentIterator(it) => mark(it), #[cfg(feature = "intl")] - Self::ListFormat(_) | Self::Locale(_) | Self::Segmenter(_) => {} + Self::ListFormat(_) + | Self::Locale(_) + | Self::Segmenter(_) + | Self::PluralRules(_) => {} Self::RegExp(_) | Self::BigInt(_) | Self::Boolean(_) @@ -829,6 +837,16 @@ impl ObjectData { internal_methods: &ORDINARY_INTERNAL_METHODS, } } + + /// Create the `PluralRules` object data + #[cfg(feature = "intl")] + #[must_use] + pub fn plural_rules(plural_rules: PluralRules) -> Self { + Self { + kind: ObjectKind::PluralRules(plural_rules), + internal_methods: &ORDINARY_INTERNAL_METHODS, + } + } } impl Debug for ObjectKind { @@ -885,6 +903,8 @@ impl Debug for ObjectKind { Self::Segments(_) => "Segments", #[cfg(feature = "intl")] Self::SegmentIterator(_) => "SegmentIterator", + #[cfg(feature = "intl")] + Self::PluralRules(_) => "PluralRules", }) } } @@ -1786,6 +1806,27 @@ impl Object { } } + /// Gets the `PluralRules` data if the object is a `PluralRules`. + #[inline] + #[must_use] + #[cfg(feature = "intl")] + pub const fn as_plural_rules(&self) -> Option<&PluralRules> { + match &self.kind { + ObjectKind::PluralRules(it) => Some(it), + _ => None, + } + } + + /// Gets a mutable reference to the `PluralRules` data if the object is a `PluralRules`. + #[inline] + #[cfg(feature = "intl")] + pub fn as_plural_rules_mut(&mut self) -> Option<&mut PluralRules> { + match &mut self.kind { + ObjectKind::PluralRules(plural_rules) => Some(plural_rules), + _ => None, + } + } + /// Return `true` if it is a native object and the native type is `T`. #[must_use] pub fn is(&self) -> bool @@ -2146,6 +2187,12 @@ impl<'ctx, 'host> ObjectInitializer<'ctx, 'host> { pub fn build(&mut self) -> JsObject { self.object.clone() } + + /// Gets the context used to create the object. + #[inline] + pub fn context(&mut self) -> &mut Context<'host> { + self.context + } } /// Builder for creating constructors objects, like `Array`. diff --git a/test262_config.toml b/test262_config.toml index 032d535c9f8..53e8623d077 100644 --- a/test262_config.toml +++ b/test262_config.toml @@ -17,15 +17,13 @@ features = [ "Intl.DisplayNames", "Intl.RelativeTimeFormat", "Intl-enumeration", + "Intl.NumberFormat-v3", ### Pending proposals # https://github.com/tc39/proposal-intl-locale-info "Intl.Locale-info", - # https://github.com/tc39/proposal-intl-numberformat-v3 - "Intl.NumberFormat-v3", - # https://github.com/tc39/proposal-regexp-legacy-features "legacy-regexp", From e43d4b1e7fc4def976fb133a94fa67dcee44b71a Mon Sep 17 00:00:00 2001 From: jedel1043 Date: Sun, 24 Sep 2023 05:21:49 -0600 Subject: [PATCH 2/5] cargo fmt --- boa_engine/src/builtins/intl/mod.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/boa_engine/src/builtins/intl/mod.rs b/boa_engine/src/builtins/intl/mod.rs index fb0dac97912..4fce3661f9c 100644 --- a/boa_engine/src/builtins/intl/mod.rs +++ b/boa_engine/src/builtins/intl/mod.rs @@ -32,7 +32,7 @@ pub(crate) mod segmenter; pub(crate) use self::{ collator::Collator, date_time_format::DateTimeFormat, list_format::ListFormat, locale::Locale, - segmenter::Segmenter, plural_rules::PluralRules + plural_rules::PluralRules, segmenter::Segmenter, }; mod options; @@ -77,7 +77,11 @@ impl IntrinsicObject for Intl { ) .static_property( PluralRules::NAME, - realm.intrinsics().constructors().plural_rules().constructor(), + realm + .intrinsics() + .constructors() + .plural_rules() + .constructor(), PluralRules::ATTRIBUTE, ) .static_property( From 2b1af66f399284aa304fac045ece43232c433292 Mon Sep 17 00:00:00 2001 From: jedel1043 Date: Sun, 24 Sep 2023 16:43:11 -0600 Subject: [PATCH 3/5] Move options utils to builtins module --- boa_engine/src/builtins/intl/collator/mod.rs | 6 +- .../src/builtins/intl/collator/options.rs | 6 +- .../src/builtins/intl/date_time_format.rs | 6 +- .../src/builtins/intl/list_format/mod.rs | 7 +- .../src/builtins/intl/list_format/options.rs | 4 +- boa_engine/src/builtins/intl/locale/mod.rs | 4 +- .../src/builtins/intl/locale/options.rs | 2 +- boa_engine/src/builtins/intl/locale/utils.rs | 3 +- .../builtins/intl/number_format/options.rs | 69 +------ .../src/builtins/intl/number_format/utils.rs | 9 +- boa_engine/src/builtins/intl/options.rs | 114 +---------- .../src/builtins/intl/plural_rules/mod.rs | 7 +- .../src/builtins/intl/plural_rules/options.rs | 2 +- boa_engine/src/builtins/intl/segmenter/mod.rs | 7 +- .../src/builtins/intl/segmenter/options.rs | 4 +- boa_engine/src/builtins/mod.rs | 4 + boa_engine/src/builtins/options.rs | 181 ++++++++++++++++++ 17 files changed, 233 insertions(+), 202 deletions(-) create mode 100644 boa_engine/src/builtins/options.rs diff --git a/boa_engine/src/builtins/intl/collator/mod.rs b/boa_engine/src/builtins/intl/collator/mod.rs index ccbf1cd2dbe..6a672f604b2 100644 --- a/boa_engine/src/builtins/intl/collator/mod.rs +++ b/boa_engine/src/builtins/intl/collator/mod.rs @@ -11,7 +11,9 @@ use icu_locid::{ use icu_provider::DataLocale; use crate::{ - builtins::{BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject}, + builtins::{ + options::get_option, BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, + }, context::{ intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, BoaProvider, @@ -30,7 +32,7 @@ use crate::{ use super::{ locale::{canonicalize_locale_list, resolve_locale, supported_locales, validate_extension}, - options::{coerce_options_to_object, get_option, IntlOptions, LocaleMatcher}, + options::{coerce_options_to_object, IntlOptions, LocaleMatcher}, Service, }; diff --git a/boa_engine/src/builtins/intl/collator/options.rs b/boa_engine/src/builtins/intl/collator/options.rs index 51a6c6bdeb5..8cdcbde0b98 100644 --- a/boa_engine/src/builtins/intl/collator/options.rs +++ b/boa_engine/src/builtins/intl/collator/options.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use icu_collator::{CaseFirst, CaseLevel, Strength}; use crate::{ - builtins::intl::options::{OptionType, OptionTypeParsable}, + builtins::options::{OptionType, ParsableOptionType}, Context, JsNativeError, JsResult, JsValue, }; @@ -50,7 +50,7 @@ impl FromStr for Sensitivity { } } -impl OptionTypeParsable for Sensitivity {} +impl ParsableOptionType for Sensitivity {} #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub(crate) enum Usage { @@ -80,7 +80,7 @@ impl FromStr for Usage { } } -impl OptionTypeParsable for Usage {} +impl ParsableOptionType for Usage {} impl OptionType for CaseFirst { fn from_value(value: JsValue, context: &mut Context<'_>) -> JsResult { diff --git a/boa_engine/src/builtins/intl/date_time_format.rs b/boa_engine/src/builtins/intl/date_time_format.rs index 4b44d2f2c9b..35394f04654 100644 --- a/boa_engine/src/builtins/intl/date_time_format.rs +++ b/boa_engine/src/builtins/intl/date_time_format.rs @@ -8,7 +8,9 @@ //! [spec]: https://tc39.es/ecma402/#datetimeformat-objects use crate::{ - builtins::{BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject}, + builtins::{ + options::OptionType, BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, + }, context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, error::JsNativeError, js_string, @@ -22,8 +24,6 @@ use boa_gc::{Finalize, Trace}; use boa_profiler::Profiler; use icu_datetime::options::preferences::HourCycle; -use super::options::OptionType; - impl OptionType for HourCycle { fn from_value(value: JsValue, context: &mut Context<'_>) -> JsResult { match value.to_string(context)?.to_std_string_escaped().as_str() { diff --git a/boa_engine/src/builtins/intl/list_format/mod.rs b/boa_engine/src/builtins/intl/list_format/mod.rs index 6b4954bfa2f..2859095a2ad 100644 --- a/boa_engine/src/builtins/intl/list_format/mod.rs +++ b/boa_engine/src/builtins/intl/list_format/mod.rs @@ -6,7 +6,10 @@ use icu_locid::Locale; use icu_provider::DataLocale; use crate::{ - builtins::{Array, BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject}, + builtins::{ + options::{get_option, get_options_object}, + Array, BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, + }, context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, object::{internal_methods::get_prototype_from_constructor, JsObject, ObjectData}, property::Attribute, @@ -18,7 +21,7 @@ use crate::{ use super::{ locale::{canonicalize_locale_list, resolve_locale, supported_locales}, - options::{get_option, get_options_object, IntlOptions, LocaleMatcher}, + options::{IntlOptions, LocaleMatcher}, Service, }; diff --git a/boa_engine/src/builtins/intl/list_format/options.rs b/boa_engine/src/builtins/intl/list_format/options.rs index 8a7e6edfdb1..9d1fdad1622 100644 --- a/boa_engine/src/builtins/intl/list_format/options.rs +++ b/boa_engine/src/builtins/intl/list_format/options.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use icu_list::ListLength; use crate::{ - builtins::intl::options::{OptionType, OptionTypeParsable}, + builtins::options::{OptionType, ParsableOptionType}, Context, JsNativeError, JsResult, JsValue, }; @@ -37,7 +37,7 @@ impl FromStr for ListFormatType { } } -impl OptionTypeParsable for ListFormatType {} +impl ParsableOptionType for ListFormatType {} impl OptionType for ListLength { fn from_value(value: JsValue, context: &mut Context<'_>) -> JsResult { diff --git a/boa_engine/src/builtins/intl/locale/mod.rs b/boa_engine/src/builtins/intl/locale/mod.rs index f8ee0809cf5..dcfea8a6082 100644 --- a/boa_engine/src/builtins/intl/locale/mod.rs +++ b/boa_engine/src/builtins/intl/locale/mod.rs @@ -1,4 +1,4 @@ -use crate::{realm::Realm, string::utf16}; +use crate::{builtins::options::get_option, realm::Realm, string::utf16}; use boa_profiler::Profiler; use icu_collator::CaseFirst; use icu_datetime::options::preferences::HourCycle; @@ -26,7 +26,7 @@ use crate::{ Context, JsArgs, JsNativeError, JsResult, JsString, JsValue, }; -use super::options::{coerce_options_to_object, get_option}; +use super::options::coerce_options_to_object; #[derive(Debug, Clone)] pub(crate) struct Locale; diff --git a/boa_engine/src/builtins/intl/locale/options.rs b/boa_engine/src/builtins/intl/locale/options.rs index 17660aad5b2..b42526f0af0 100644 --- a/boa_engine/src/builtins/intl/locale/options.rs +++ b/boa_engine/src/builtins/intl/locale/options.rs @@ -1,6 +1,6 @@ use icu_locid::extensions::unicode::Value; -use crate::{builtins::intl::options::OptionType, Context, JsNativeError}; +use crate::{builtins::options::OptionType, Context, JsNativeError}; impl OptionType for Value { fn from_value(value: crate::JsValue, context: &mut Context<'_>) -> crate::JsResult { diff --git a/boa_engine/src/builtins/intl/locale/utils.rs b/boa_engine/src/builtins/intl/locale/utils.rs index 226cb126c30..f59efc5491e 100644 --- a/boa_engine/src/builtins/intl/locale/utils.rs +++ b/boa_engine/src/builtins/intl/locale/utils.rs @@ -1,9 +1,10 @@ use crate::{ builtins::{ intl::{ - options::{coerce_options_to_object, get_option, IntlOptions, LocaleMatcher}, + options::{coerce_options_to_object, IntlOptions, LocaleMatcher}, Service, }, + options::get_option, Array, }, context::{icu::Icu, BoaProvider}, diff --git a/boa_engine/src/builtins/intl/number_format/options.rs b/boa_engine/src/builtins/intl/number_format/options.rs index 1941df10138..04f3ed2010a 100644 --- a/boa_engine/src/builtins/intl/number_format/options.rs +++ b/boa_engine/src/builtins/intl/number_format/options.rs @@ -1,6 +1,6 @@ use std::fmt; -use crate::builtins::intl::options::OptionTypeParsable; +use crate::builtins::options::{ParsableOptionType, RoundingMode}; #[derive(Debug)] pub(crate) struct DigitFormatOptions { @@ -44,7 +44,7 @@ impl std::str::FromStr for Notation { } } -impl OptionTypeParsable for Notation {} +impl ParsableOptionType for Notation {} #[derive(Debug, Copy, Clone, Default, Eq, PartialEq)] pub(crate) enum RoundingPriority { @@ -76,7 +76,7 @@ impl std::str::FromStr for RoundingPriority { } } -impl OptionTypeParsable for RoundingPriority {} +impl ParsableOptionType for RoundingPriority {} impl fmt::Display for RoundingPriority { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -89,67 +89,6 @@ impl fmt::Display for RoundingPriority { } } -#[derive(Debug, Copy, Clone, Default)] -pub(crate) enum RoundingMode { - Ceil, - Floor, - Expand, - Trunc, - HalfCeil, - HalfFloor, - #[default] - HalfExpand, - HalfTrunc, - HalfEven, -} - -#[derive(Debug)] -pub(crate) struct ParseRoundingModeError; - -impl fmt::Display for ParseRoundingModeError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("provided string was not a valid rounding mode") - } -} - -impl std::str::FromStr for RoundingMode { - type Err = ParseRoundingModeError; - - fn from_str(s: &str) -> Result { - match s { - "ceil" => Ok(Self::Ceil), - "floor" => Ok(Self::Floor), - "expand" => Ok(Self::Expand), - "trunc" => Ok(Self::Trunc), - "halfCeil" => Ok(Self::HalfCeil), - "halfFloor" => Ok(Self::HalfFloor), - "halfExpand" => Ok(Self::HalfExpand), - "halfTrunc" => Ok(Self::HalfTrunc), - "halfEven" => Ok(Self::HalfEven), - _ => Err(ParseRoundingModeError), - } - } -} - -impl OptionTypeParsable 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) - } -} - #[derive(Debug, Copy, Clone, Default, PartialEq, Eq)] pub(crate) enum TrailingZeroDisplay { #[default] @@ -178,7 +117,7 @@ impl std::str::FromStr for TrailingZeroDisplay { } } -impl OptionTypeParsable for TrailingZeroDisplay {} +impl ParsableOptionType for TrailingZeroDisplay {} impl fmt::Display for TrailingZeroDisplay { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { diff --git a/boa_engine/src/builtins/intl/number_format/utils.rs b/boa_engine/src/builtins/intl/number_format/utils.rs index b5ba7fb3e9a..860ddffff85 100644 --- a/boa_engine/src/builtins/intl/number_format/utils.rs +++ b/boa_engine/src/builtins/intl/number_format/utils.rs @@ -2,9 +2,12 @@ use boa_macros::utf16; use fixed_decimal::{FixedDecimal, FloatPrecision}; use crate::{ - builtins::intl::{ - number_format::{Extrema, RoundingMode, RoundingType, TrailingZeroDisplay}, - options::{default_number_option, get_number_option, get_option}, + builtins::{ + intl::{ + number_format::{Extrema, RoundingType, TrailingZeroDisplay}, + options::{default_number_option, get_number_option}, + }, + options::{get_option, RoundingMode}, }, Context, JsNativeError, JsObject, JsResult, }; diff --git a/boa_engine/src/builtins/intl/options.rs b/boa_engine/src/builtins/intl/options.rs index 0a6ce6c2675..7efd2201673 100644 --- a/boa_engine/src/builtins/intl/options.rs +++ b/boa_engine/src/builtins/intl/options.rs @@ -3,8 +3,9 @@ use std::{fmt::Display, str::FromStr}; use num_traits::FromPrimitive; use crate::{ + builtins::options::ParsableOptionType, object::{JsObject, ObjectData}, - Context, JsNativeError, JsResult, JsString, JsValue, + Context, JsNativeError, JsResult, JsValue, }; /// `IntlOptions` aggregates the `locale_matcher` selector and any other object @@ -17,50 +18,6 @@ pub(super) struct IntlOptions { pub(super) service_options: O, } -/// A type used as an option parameter inside the `Intl` [spec]. -/// -/// [spec]: https://tc39.es/ecma402 -pub(super) trait OptionType: Sized { - /// Parses a [`JsValue`] into an instance of `Self`. - /// - /// Roughly equivalent to the algorithm steps of [9.12.13.3-7][spec], but allows for parsing - /// steps instead of returning a pure string, number or boolean. - /// - /// [spec]: https://tc39.es/ecma402/#sec-getoption - fn from_value(value: JsValue, context: &mut Context<'_>) -> JsResult; -} - -pub(super) trait OptionTypeParsable: FromStr {} - -impl OptionType for T -where - T::Err: Display, -{ - fn from_value(value: JsValue, context: &mut Context<'_>) -> JsResult { - value - .to_string(context)? - .to_std_string_escaped() - .parse::() - .map_err(|err| JsNativeError::range().with_message(err.to_string()).into()) - } -} - -impl OptionType for bool { - fn from_value(value: JsValue, _: &mut Context<'_>) -> JsResult { - // 5. If type is "boolean", then - // a. Set value to ! ToBoolean(value). - Ok(value.to_boolean()) - } -} - -impl OptionType for JsString { - fn from_value(value: JsValue, context: &mut Context<'_>) -> JsResult { - // 6. If type is "string", then - // a. Set value to ? ToString(value). - value.to_string(context) - } -} - #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] pub(super) enum LocaleMatcher { Lookup, @@ -89,47 +46,7 @@ impl FromStr for LocaleMatcher { } } -impl OptionTypeParsable for LocaleMatcher {} - -/// Abstract operation [`GetOption ( options, property, type, values, fallback )`][spec] -/// -/// Extracts the value of the property named `property` from the provided `options` object, -/// converts it to the required `type` and checks whether it is one of a `List` of allowed -/// `values`. If `values` is undefined, there is no fixed set of values and any is permitted. -/// If the value is `undefined`, `required` determines if the function should return `None` or -/// an `Err`. Use [`Option::unwrap_or`] and friends to manage the default value. -/// -/// This is a safer alternative to `GetOption`, which tries to parse from the -/// provided property a valid variant of the provided type `T`. It doesn't accept -/// a `type` parameter since the type can specify in its implementation of [`TryFrom`] whether -/// it wants to parse from a [`str`] or convert directly from a boolean or number. -/// -/// [spec]: https://tc39.es/ecma402/#sec-getoption -pub(super) fn get_option( - options: &JsObject, - property: &[u16], - required: bool, - context: &mut Context<'_>, -) -> JsResult> { - // 1. Let value be ? Get(options, property). - let value = options.get(property, context)?; - - // 2. If value is undefined, then - if value.is_undefined() { - return if required { - // a. If default is required, throw a RangeError exception. - Err(JsNativeError::range() - .with_message("GetOption: option value cannot be undefined") - .into()) - } else { - // b. Return default. - Ok(None) - }; - } - - // The steps 3 to 7 must be made for each `OptionType`. - T::from_value(value, context).map(Some) -} +impl ParsableOptionType for LocaleMatcher {} /// Abstract operation `GetNumberOption ( options, property, minimum, maximum, fallback )` /// @@ -194,31 +111,6 @@ where Ok(T::from_f64(value)) } -/// Abstract operation [`GetOptionsObject ( options )`][spec] -/// -/// Returns a [`JsObject`] suitable for use with [`get_option`], either `options` itself or a default empty -/// `JsObject`. It throws a `TypeError` if `options` is not undefined and not a `JsObject`. -/// -/// [spec]: https://tc39.es/ecma402/#sec-getoptionsobject -pub(super) fn get_options_object(options: &JsValue) -> JsResult { - match options { - // If options is undefined, then - JsValue::Undefined => { - // a. Return OrdinaryObjectCreate(null). - Ok(JsObject::from_proto_and_data(None, ObjectData::ordinary())) - } - // 2. If Type(options) is Object, then - JsValue::Object(obj) => { - // a. Return options. - Ok(obj.clone()) - } - // 3. Throw a TypeError exception. - _ => Err(JsNativeError::typ() - .with_message("GetOptionsObject: provided options is not an object") - .into()), - } -} - /// Abstract operation [`CoerceOptionsToObject ( options )`][spec] /// /// Coerces `options` into a [`JsObject`] suitable for use with [`get_option`], defaulting to an empty diff --git a/boa_engine/src/builtins/intl/plural_rules/mod.rs b/boa_engine/src/builtins/intl/plural_rules/mod.rs index 88a06d6c6ae..423e22d1a96 100644 --- a/boa_engine/src/builtins/intl/plural_rules/mod.rs +++ b/boa_engine/src/builtins/intl/plural_rules/mod.rs @@ -10,7 +10,10 @@ use icu_plurals::{ use icu_provider::DataLocale; use crate::{ - builtins::{Array, BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject}, + builtins::{ + options::get_option, Array, BuiltInBuilder, BuiltInConstructor, BuiltInObject, + IntrinsicObject, + }, context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, js_string, object::{internal_methods::get_prototype_from_constructor, ObjectData, ObjectInitializer}, @@ -25,7 +28,7 @@ use super::{ f64_to_formatted_fixed_decimal, get_digit_format_options, DigitFormatOptions, Extrema, Notation, }, - options::{coerce_options_to_object, get_option, IntlOptions, LocaleMatcher}, + options::{coerce_options_to_object, IntlOptions, LocaleMatcher}, Service, }; diff --git a/boa_engine/src/builtins/intl/plural_rules/options.rs b/boa_engine/src/builtins/intl/plural_rules/options.rs index f70f247bef6..6c8341bce96 100644 --- a/boa_engine/src/builtins/intl/plural_rules/options.rs +++ b/boa_engine/src/builtins/intl/plural_rules/options.rs @@ -1,6 +1,6 @@ use icu_plurals::PluralRuleType; -use crate::{builtins::intl::options::OptionType, Context, JsNativeError, JsResult, JsValue}; +use crate::{builtins::options::OptionType, Context, JsNativeError, JsResult, JsValue}; impl OptionType for PluralRuleType { fn from_value(value: JsValue, context: &mut Context<'_>) -> JsResult { diff --git a/boa_engine/src/builtins/intl/segmenter/mod.rs b/boa_engine/src/builtins/intl/segmenter/mod.rs index 258561f868f..b264d86f634 100644 --- a/boa_engine/src/builtins/intl/segmenter/mod.rs +++ b/boa_engine/src/builtins/intl/segmenter/mod.rs @@ -6,7 +6,10 @@ use icu_locid::Locale; use icu_segmenter::provider::WordBreakDataV1Marker; use crate::{ - builtins::{BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject}, + builtins::{ + options::{get_option, get_options_object}, + BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, + }, context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, js_string, object::{ @@ -26,7 +29,7 @@ pub(crate) use segments::*; use super::{ locale::{canonicalize_locale_list, resolve_locale, supported_locales}, - options::{get_option, get_options_object, IntlOptions, LocaleMatcher}, + options::{IntlOptions, LocaleMatcher}, Service, }; diff --git a/boa_engine/src/builtins/intl/segmenter/options.rs b/boa_engine/src/builtins/intl/segmenter/options.rs index c20f4f6ed5d..ca36fc63890 100644 --- a/boa_engine/src/builtins/intl/segmenter/options.rs +++ b/boa_engine/src/builtins/intl/segmenter/options.rs @@ -1,6 +1,6 @@ use std::fmt::Display; -use crate::builtins::intl::options::OptionTypeParsable; +use crate::builtins::options::ParsableOptionType; #[derive(Debug, Clone, Copy, Default)] pub(crate) enum Granularity { @@ -43,4 +43,4 @@ impl std::str::FromStr for Granularity { } } -impl OptionTypeParsable for Granularity {} +impl ParsableOptionType for Granularity {} diff --git a/boa_engine/src/builtins/mod.rs b/boa_engine/src/builtins/mod.rs index 02af65ee837..fe776aef1ce 100644 --- a/boa_engine/src/builtins/mod.rs +++ b/boa_engine/src/builtins/mod.rs @@ -39,6 +39,10 @@ pub mod escape; #[cfg(feature = "intl")] pub mod intl; +// TODO: remove `cfg` when `Temporal` gets to stage 4. +#[cfg(any(feature = "intl", feature = "experimental"))] +pub(crate) mod options; + pub(crate) use self::{ array::Array, async_function::AsyncFunction, diff --git a/boa_engine/src/builtins/options.rs b/boa_engine/src/builtins/options.rs new file mode 100644 index 00000000000..e821363f4c3 --- /dev/null +++ b/boa_engine/src/builtins/options.rs @@ -0,0 +1,181 @@ +//! Utilities to parse, validate and get options in builtins. + +use std::{fmt, str::FromStr}; + +use crate::{ + object::{JsObject, ObjectData}, + Context, JsNativeError, JsResult, JsString, JsValue, +}; + +/// A type used as an option parameter for [`get_option`]. +pub(crate) trait OptionType: Sized { + /// Parses a [`JsValue`] into an instance of `Self`. + /// + /// Roughly equivalent to the algorithm steps of [9.12.13.3-7][spec], but allows for parsing + /// steps instead of returning a pure string, number or boolean. + /// + /// [spec]: https://tc39.es/ecma402/#sec-getoption + fn from_value(value: JsValue, context: &mut Context<'_>) -> JsResult; +} + +/// A type that implements [`OptionType`] by parsing a string. +/// +/// This automatically implements `OptionType` for a type if the type implements `FromStr`. +pub(crate) trait ParsableOptionType: FromStr {} + +impl OptionType for T +where + T::Err: fmt::Display, +{ + fn from_value(value: JsValue, context: &mut Context<'_>) -> JsResult { + value + .to_string(context)? + .to_std_string_escaped() + .parse::() + .map_err(|err| JsNativeError::range().with_message(err.to_string()).into()) + } +} + +/// Abstract operation [`GetOption ( options, property, type, values, fallback )`][spec] +/// +/// Extracts the value of the property named `property` from the provided `options` object, +/// converts it to the required `type` and checks whether it is one of a `List` of allowed +/// `values`. If `values` is undefined, there is no fixed set of values and any is permitted. +/// If the value is `undefined`, `required` determines if the function should return `None` or +/// an `Err`. Use [`Option::unwrap_or`] and friends to manage the default value. +/// +/// This is a safer alternative to `GetOption`, which tries to parse from the +/// provided property a valid variant of the provided type `T`. It doesn't accept +/// a `type` parameter since the type can specify in its implementation of [`OptionType`] whether +/// it wants to parse from a [`str`] or convert directly from a boolean or number. +/// +/// [spec]: https://tc39.es/ecma402/#sec-getoption +pub(crate) fn get_option( + options: &JsObject, + property: &[u16], + required: bool, + context: &mut Context<'_>, +) -> JsResult> { + // 1. Let value be ? Get(options, property). + let value = options.get(property, context)?; + + // 2. If value is undefined, then + if value.is_undefined() { + return if required { + // a. If default is required, throw a RangeError exception. + Err(JsNativeError::range() + .with_message("GetOption: option value cannot be undefined") + .into()) + } else { + // b. Return default. + Ok(None) + }; + } + + // The steps 3 to 7 must be made for each `OptionType`. + T::from_value(value, context).map(Some) +} + +/// Abstract operation [`GetOptionsObject ( options )`][spec] +/// +/// Returns a [`JsObject`] suitable for use with [`get_option`], either `options` itself or a +/// default empty `JsObject`. It throws a `TypeError` if `options` is not undefined and not a `JsObject`. +/// +/// [spec]: https://tc39.es/ecma402/#sec-getoptionsobject +pub(crate) fn get_options_object(options: &JsValue) -> JsResult { + match options { + // If options is undefined, then + JsValue::Undefined => { + // a. Return OrdinaryObjectCreate(null). + Ok(JsObject::from_proto_and_data(None, ObjectData::ordinary())) + } + // 2. If Type(options) is Object, then + JsValue::Object(obj) => { + // a. Return options. + Ok(obj.clone()) + } + // 3. Throw a TypeError exception. + _ => Err(JsNativeError::typ() + .with_message("GetOptionsObject: provided options is not an object") + .into()), + } +} + +// Common options used in several builtins + +impl OptionType for bool { + fn from_value(value: JsValue, _: &mut Context<'_>) -> JsResult { + // 5. If type is "boolean", then + // a. Set value to ! ToBoolean(value). + Ok(value.to_boolean()) + } +} + +impl OptionType for JsString { + fn from_value(value: JsValue, context: &mut Context<'_>) -> JsResult { + // 6. If type is "string", then + // a. Set value to ? ToString(value). + value.to_string(context) + } +} + +#[derive(Debug, Copy, Clone, Default)] +pub(crate) enum RoundingMode { + Ceil, + Floor, + Expand, + Trunc, + HalfCeil, + HalfFloor, + #[default] + HalfExpand, + HalfTrunc, + HalfEven, +} + +#[derive(Debug)] +pub(crate) struct ParseRoundingModeError; + +impl fmt::Display for ParseRoundingModeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("provided string was not a valid rounding mode") + } +} + +impl FromStr for RoundingMode { + type Err = ParseRoundingModeError; + + fn from_str(s: &str) -> Result { + match s { + "ceil" => Ok(Self::Ceil), + "floor" => Ok(Self::Floor), + "expand" => Ok(Self::Expand), + "trunc" => Ok(Self::Trunc), + "halfCeil" => Ok(Self::HalfCeil), + "halfFloor" => Ok(Self::HalfFloor), + "halfExpand" => Ok(Self::HalfExpand), + "halfTrunc" => Ok(Self::HalfTrunc), + "halfEven" => Ok(Self::HalfEven), + _ => Err(ParseRoundingModeError), + } + } +} + +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) + } +} From 941a632a9c90d1fe2c65322c69504525804fd02e Mon Sep 17 00:00:00 2001 From: jedel1043 Date: Sun, 24 Sep 2023 17:00:38 -0600 Subject: [PATCH 4/5] Fix docs --- boa_engine/src/builtins/intl/options.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/boa_engine/src/builtins/intl/options.rs b/boa_engine/src/builtins/intl/options.rs index 7efd2201673..73168d4e888 100644 --- a/boa_engine/src/builtins/intl/options.rs +++ b/boa_engine/src/builtins/intl/options.rs @@ -113,12 +113,14 @@ where /// Abstract operation [`CoerceOptionsToObject ( options )`][spec] /// -/// Coerces `options` into a [`JsObject`] suitable for use with [`get_option`], defaulting to an empty -/// `JsObject`. +/// Coerces `options` into a [`JsObject`] suitable for use with [`get_option`], defaulting to an +/// empty `JsObject`. /// Because it coerces non-null primitive values into objects, its use is discouraged for new /// functionality in favour of [`get_options_object`]. /// /// [spec]: https://tc39.es/ecma402/#sec-coerceoptionstoobject +/// [`get_option`]: crate::builtins::options::get_option +/// [`get_options_object`]: crate::builtins::options::get_options_object pub(super) fn coerce_options_to_object( options: &JsValue, context: &mut Context<'_>, From bb2938107319ba30ef9624ef22edde4897ca678b Mon Sep 17 00:00:00 2001 From: jedel1043 Date: Sun, 24 Sep 2023 19:28:28 -0600 Subject: [PATCH 5/5] Apply review --- boa_engine/src/builtins/options.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/boa_engine/src/builtins/options.rs b/boa_engine/src/builtins/options.rs index e821363f4c3..1e6b1bb3d33 100644 --- a/boa_engine/src/builtins/options.rs +++ b/boa_engine/src/builtins/options.rs @@ -2,10 +2,7 @@ use std::{fmt, str::FromStr}; -use crate::{ - object::{JsObject, ObjectData}, - Context, JsNativeError, JsResult, JsString, JsValue, -}; +use crate::{object::JsObject, Context, JsNativeError, JsResult, JsString, JsValue}; /// A type used as an option parameter for [`get_option`]. pub(crate) trait OptionType: Sized { @@ -87,7 +84,7 @@ pub(crate) fn get_options_object(options: &JsValue) -> JsResult { // If options is undefined, then JsValue::Undefined => { // a. Return OrdinaryObjectCreate(null). - Ok(JsObject::from_proto_and_data(None, ObjectData::ordinary())) + Ok(JsObject::with_null_proto()) } // 2. If Type(options) is Object, then JsValue::Object(obj) => {