Browse Source

Implement `Intl.PluralRules` (#3298)

* Implement `PluralRules`

* cargo fmt

* Move options utils to builtins module

* Fix docs

* Apply review
pull/3300/head
José Julián Espina 1 year ago committed by GitHub
parent
commit
25c120b5f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      Cargo.lock
  2. 2
      boa_engine/Cargo.toml
  3. 6
      boa_engine/src/builtins/intl/collator/mod.rs
  4. 24
      boa_engine/src/builtins/intl/collator/options.rs
  5. 6
      boa_engine/src/builtins/intl/date_time_format.rs
  6. 7
      boa_engine/src/builtins/intl/list_format/mod.rs
  7. 4
      boa_engine/src/builtins/intl/list_format/options.rs
  8. 4
      boa_engine/src/builtins/intl/locale/mod.rs
  9. 2
      boa_engine/src/builtins/intl/locale/options.rs
  10. 3
      boa_engine/src/builtins/intl/locale/utils.rs
  11. 13
      boa_engine/src/builtins/intl/mod.rs
  12. 4
      boa_engine/src/builtins/intl/number_format/mod.rs
  13. 180
      boa_engine/src/builtins/intl/number_format/options.rs
  14. 408
      boa_engine/src/builtins/intl/number_format/utils.rs
  15. 170
      boa_engine/src/builtins/intl/options.rs
  16. 412
      boa_engine/src/builtins/intl/plural_rules/mod.rs
  17. 15
      boa_engine/src/builtins/intl/plural_rules/options.rs
  18. 11
      boa_engine/src/builtins/intl/segmenter/mod.rs
  19. 4
      boa_engine/src/builtins/intl/segmenter/options.rs
  20. 5
      boa_engine/src/builtins/mod.rs
  21. 178
      boa_engine/src/builtins/options.rs
  22. 16
      boa_engine/src/context/icu.rs
  23. 17
      boa_engine/src/context/intrinsics.rs
  24. 49
      boa_engine/src/object/mod.rs
  25. 4
      test262_config.toml

6
Cargo.lock generated

@ -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",
]

2
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"

6
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,
};

24
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::options::{OptionType, ParsableOptionType},
Context, JsNativeError, JsResult, JsValue,
};
#[derive(Debug, Clone, Copy)]
pub(crate) enum Sensitivity {
@ -47,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 {
@ -77,4 +80,17 @@ 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<Self> {
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()),
}
}
}

6
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<Self> {
match value.to_string(context)?.to_std_string_escaped().as_str() {

7
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,
};

4
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<Self> {

4
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;

2
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<Self> {

3
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},

13
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,
plural_rules::PluralRules, segmenter::Segmenter,
};
mod options;
@ -73,6 +75,15 @@ 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

4
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::*;

180
boa_engine/src/builtins/intl/number_format/options.rs

@ -0,0 +1,180 @@
use std::fmt;
use crate::builtins::options::{ParsableOptionType, RoundingMode};
#[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<Self, Self::Err> {
match s {
"standard" => Ok(Self::Standard),
"scientific" => Ok(Self::Scientific),
"engineering" => Ok(Self::Engineering),
"compact" => Ok(Self::Compact),
_ => Err(ParseNotationError),
}
}
}
impl ParsableOptionType 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<Self, Self::Err> {
match s {
"auto" => Ok(Self::Auto),
"morePrecision" => Ok(Self::MorePrecision),
"lessPrecision" => Ok(Self::LessPrecision),
_ => Err(ParseRoundingPriorityError),
}
}
}
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]
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<Self, Self::Err> {
match s {
"auto" => Ok(Self::Auto),
"stripIfInteger" => Ok(Self::StripIfInteger),
_ => Err(ParseTrailingZeroDisplayError),
}
}
}
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",
}
.fmt(f)
}
}
#[derive(Debug, Copy, Clone)]
pub(crate) struct Extrema<T> {
pub(crate) minimum: T,
pub(crate) maximum: T,
}
#[derive(Debug, Copy, Clone)]
pub(crate) enum RoundingType {
MorePrecision {
significant_digits: Extrema<u8>,
fraction_digits: Extrema<u8>,
},
LessPrecision {
significant_digits: Extrema<u8>,
fraction_digits: Extrema<u8>,
},
SignificantDigits(Extrema<u8>),
FractionDigits(Extrema<u8>),
}
impl RoundingType {
/// Gets the significant digit limits of the rounding type, or `None` otherwise.
pub(crate) const fn significant_digits(self) -> Option<Extrema<u8>> {
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<Extrema<u8>> {
match self {
Self::MorePrecision {
fraction_digits, ..
}
| Self::LessPrecision {
fraction_digits, ..
}
| Self::FractionDigits(fraction_digits) => Some(fraction_digits),
Self::SignificantDigits(_) => None,
}
}
}

408
boa_engine/src/builtins/intl/number_format/utils.rs

@ -0,0 +1,408 @@
use boa_macros::utf16;
use fixed_decimal::{FixedDecimal, FloatPrecision};
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};
/// Abstract operation [`SetNumberFormatDigitOptions ( intlObj, options, mnfdDefault, mxfdDefault, notation )`][spec].
///
/// Gets the digit format options of the number formatter from the options object and the requested notation.
///
/// [spec]: https://tc39.es/ecma402/#sec-setnfdigitoptions
pub(crate) fn get_digit_format_options(
options: &JsObject,
min_float_digits_default: u8,
mut max_float_digits_default: u8,
notation: Notation,
context: &mut Context<'_>,
) -> JsResult<DigitFormatOptions> {
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),
}
}
// <https://tc39.es/ecma402/#sec-torawprecision>
fn to_raw_precision(
number: &mut FixedDecimal,
min_precision: u8,
max_precision: u8,
rounding_mode: RoundingMode,
) -> i16 {
let msb = *number.magnitude_range().end();
let min_msb = msb - i16::from(min_precision) + 1;
let max_msb = msb - i16::from(max_precision) + 1;
number.pad_end(min_msb);
round(number, max_msb, rounding_mode);
max_msb
}
// <https://tc39.es/ecma402/#sec-torawfixed>
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
}

170
boa_engine/src/builtins/intl/options.rs

@ -1,10 +1,11 @@
use std::{fmt::Display, str::FromStr};
use icu_collator::CaseFirst;
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<O> {
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<Self>;
}
pub(super) trait OptionTypeParsable: FromStr {}
impl<T: OptionTypeParsable> OptionType for T
where
T::Err: Display,
{
fn from_value(value: JsValue, context: &mut Context<'_>) -> JsResult<Self> {
value
.to_string(context)?
.to_std_string_escaped()
.parse::<Self>()
.map_err(|err| JsNativeError::range().with_message(err.to_string()).into())
}
}
impl OptionType for bool {
fn from_value(value: JsValue, _: &mut Context<'_>) -> JsResult<Self> {
// 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<Self> {
// 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,60 +46,7 @@ impl FromStr for LocaleMatcher {
}
}
impl OptionTypeParsable for LocaleMatcher {}
impl OptionType for CaseFirst {
fn from_value(value: JsValue, context: &mut Context<'_>) -> JsResult<Self> {
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,
/// 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<T: OptionType>(
options: &JsObject,
property: &[u16],
required: bool,
context: &mut Context<'_>,
) -> JsResult<Option<T>> {
// 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 )`
///
@ -154,21 +58,22 @@ pub(super) fn get_option<T: OptionType>(
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma402/#sec-getnumberoption
#[allow(unused)]
pub(super) fn get_number_option(
pub(super) fn get_number_option<T>(
options: &JsObject,
property: &[u16],
minimum: f64,
maximum: f64,
fallback: Option<f64>,
minimum: T,
maximum: T,
context: &mut Context<'_>,
) -> JsResult<Option<f64>> {
) -> JsResult<Option<T>>
where
T: Into<f64> + 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,66 +82,45 @@ 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<T>(
value: &JsValue,
minimum: f64,
maximum: f64,
fallback: Option<f64>,
minimum: T,
maximum: T,
context: &mut Context<'_>,
) -> JsResult<Option<f64>> {
) -> JsResult<Option<T>>
where
T: Into<f64> + 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()))
}
/// 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<JsObject> {
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()),
}
// We already asserted the range of `value` with the conditional above.
Ok(T::from_f64(value))
}
/// 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<'_>,

412
boa_engine/src/builtins/intl/plural_rules/mod.rs

@ -0,0 +1,412 @@
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::{
options::get_option, 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, 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::<Self>(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<JsValue> {
// 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).
// <https://tc39.es/ecma402/#sec-initializepluralrules>
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::<LocaleMatcher>(&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::<PluralRuleType>(&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::<Self>(
&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<JsValue> {
// 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<JsValue> {
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::<<Self as Service>::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<JsValue> {
// 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<FixedDecimal>,
}
/// 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"),
}
}

15
boa_engine/src/builtins/intl/plural_rules/options.rs

@ -0,0 +1,15 @@
use icu_plurals::PluralRuleType;
use crate::{builtins::options::OptionType, Context, JsNativeError, JsResult, JsValue};
impl OptionType for PluralRuleType {
fn from_value(value: JsValue, context: &mut Context<'_>) -> JsResult<Self> {
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()),
}
}
}

11
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,
};
@ -177,8 +180,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]

4
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 {}

5
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,
@ -269,6 +273,7 @@ impl Realm {
intl::Segmenter::init(self);
intl::segmenter::Segments::init(self);
intl::segmenter::SegmentIterator::init(self);
intl::PluralRules::init(self);
}
}
}

178
boa_engine/src/builtins/options.rs

@ -0,0 +1,178 @@
//! Utilities to parse, validate and get options in builtins.
use std::{fmt, str::FromStr};
use crate::{object::JsObject, 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<Self>;
}
/// 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<T: ParsableOptionType> OptionType for T
where
T::Err: fmt::Display,
{
fn from_value(value: JsValue, context: &mut Context<'_>) -> JsResult<Self> {
value
.to_string(context)?
.to_std_string_escaped()
.parse::<Self>()
.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<T: OptionType>(
options: &JsObject,
property: &[u16],
required: bool,
context: &mut Context<'_>,
) -> JsResult<Option<T>> {
// 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<JsObject> {
match options {
// If options is undefined, then
JsValue::Undefined => {
// a. Return OrdinaryObjectCreate(null).
Ok(JsObject::with_null_proto())
}
// 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<Self> {
// 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<Self> {
// 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<Self, Self::Err> {
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)
}
}

16
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<StringNormalizers, NormalizerError> {
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<PluralRules, PluralsError> {
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.

17
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

49
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<T>(&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`.

4
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",

Loading…
Cancel
Save