Browse Source

Bump ICU4X to 1.4 and finish Intl impls with new APIs (#3469)

* Migrate to workspace lints

* Fix last warnings

* Bump ICU4X@1.4 and implement missing APIs
pull/3468/head
José Julián Espina 12 months ago committed by GitHub
parent
commit
a8aad0bd44
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 458
      Cargo.lock
  2. 35
      Cargo.toml
  3. 6
      boa_engine/Cargo.toml
  4. 4
      boa_engine/src/builtins/intl/locale/utils.rs
  5. 4
      boa_engine/src/builtins/intl/number_format/options.rs
  6. 170
      boa_engine/src/builtins/intl/number_format/utils.rs
  7. 103
      boa_engine/src/builtins/intl/plural_rules/mod.rs
  8. 4
      boa_engine/src/object/mod.rs
  9. 5
      boa_icu_provider/Cargo.toml
  10. BIN
      boa_icu_provider/data/icudata.postcard
  11. 72
      boa_icu_provider/src/bin/datagen.rs

458
Cargo.lock generated

File diff suppressed because it is too large Load Diff

35
Cargo.toml

@ -66,24 +66,25 @@ thin-vec = "0.2.12"
# ICU4X # ICU4X
icu_provider = { version = "~1.3.1", default-features = false } icu_provider = { version = "~1.4.0", default-features = false }
icu_locid = { version = "~1.3.2", default-features = false } icu_locid = { version = "~1.4.0", default-features = false }
icu_locid_transform = { version = "~1.3.1", default-features = false } icu_locid_transform = { version = "~1.4.0", default-features = false }
icu_datetime = { version = "~1.3.2", default-features = false } icu_datetime = { version = "~1.4.0", default-features = false }
icu_calendar = { version = "~1.3.2", default-features = false } icu_calendar = { version = "~1.4.0", default-features = false }
icu_collator = { version = "~1.3.3", default-features = false } icu_collator = { version = "~1.4.0", default-features = false }
icu_plurals = { version = "~1.3.0", default-features = false } icu_plurals = { version = "~1.4.0", default-features = false }
icu_list = { version = "~1.3.2", default-features = false } icu_list = { version = "~1.4.0", default-features = false }
icu_casemap = { version = "~1.3.2", default-features = false } icu_casemap = { version = "~1.4.0", default-features = false }
icu_segmenter = { version = "~1.3.2", default-features = false } icu_segmenter = { version = "~1.4.0", default-features = false }
icu_datagen = { version = "~1.3.3", default-features = false } icu_datagen = { version = "~1.4.0", default-features = false }
icu_provider_adapters = { version = "~1.3.0", default-features = false } icu_provider_adapters = { version = "~1.4.0", default-features = false }
icu_provider_blob = { version = "~1.3.2", default-features = false } icu_provider_blob = { version = "~1.4.0", default-features = false }
icu_properties = { version = "~1.3.0", default-features = true } icu_properties = { version = "~1.4.0", default-features = true }
writeable = "~0.5.3" icu_normalizer = { version = "~1.4.0", default-features = true }
yoke = "~0.7.2" writeable = "~0.5.4"
yoke = "~0.7.3"
zerofrom = "~0.1.3" zerofrom = "~0.1.3"
fixed_decimal = "~0.5.4" fixed_decimal = "~0.5.5"
[workspace.metadata.workspaces] [workspace.metadata.workspaces]
allow_branch = "main" allow_branch = "main"

6
boa_engine/Cargo.toml

@ -85,7 +85,7 @@ num_enum = "0.7.1"
pollster.workspace = true pollster.workspace = true
thin-vec.workspace = true thin-vec.workspace = true
itertools = { version = "0.12.0", default-features = false } itertools = { version = "0.12.0", default-features = false }
icu_normalizer = "~1.3.0" icu_normalizer.workspace = true
paste = "1.0" paste = "1.0"
portable-atomic = "1.5.1" portable-atomic = "1.5.1"
bytemuck = { version = "1.14.0", features = ["derive"] } bytemuck = { version = "1.14.0", features = ["derive"] }
@ -102,14 +102,14 @@ icu_locid_transform = { workspace = true, default-features = false, features = [
icu_datetime = { workspace = true, default-features = false, features = ["serde", "experimental"], optional = true } icu_datetime = { workspace = true, default-features = false, features = ["serde", "experimental"], optional = true }
icu_calendar = { workspace = true, default-features = false, optional = true } icu_calendar = { workspace = true, default-features = false, optional = true }
icu_collator = { workspace = true, default-features = false, features = ["serde"], optional = true } icu_collator = { workspace = true, default-features = false, features = ["serde"], optional = true }
icu_plurals = { workspace = true, default-features = false, features = ["serde"], optional = true } icu_plurals = { workspace = true, default-features = false, features = ["serde", "experimental"], optional = true }
icu_list = { workspace = true, default-features = false, features = ["serde"], optional = true } icu_list = { workspace = true, default-features = false, features = ["serde"], optional = true }
icu_casemap = { workspace = true, default-features = false, features = ["serde"], optional = true} icu_casemap = { workspace = true, default-features = false, features = ["serde"], optional = true}
icu_segmenter = { workspace = true, default-features = false, features = ["auto", "serde"], optional = true } icu_segmenter = { workspace = true, default-features = false, features = ["auto", "serde"], optional = true }
writeable = { workspace = true, optional = true } writeable = { workspace = true, optional = true }
yoke = { workspace = true, optional = true } yoke = { workspace = true, optional = true }
zerofrom = { workspace = true, optional = true } zerofrom = { workspace = true, optional = true }
fixed_decimal = { workspace = true, features = ["ryu"], optional = true} fixed_decimal = { workspace = true, features = ["ryu", "experimental"], optional = true}
hashbrown.workspace = true hashbrown.workspace = true
[target.'cfg(all(target_family = "wasm", not(any(target_os = "emscripten", target_os = "wasi"))))'.dependencies] [target.'cfg(all(target_family = "wasm", not(any(target_os = "emscripten", target_os = "wasi"))))'.dependencies]

4
boa_engine/src/builtins/intl/locale/utils.rs

@ -325,6 +325,8 @@ where
// extension sequences removed. // extension sequences removed.
let mut locale = locale.clone(); let mut locale = locale.clone();
let id = std::mem::take(&mut locale.id); let id = std::mem::take(&mut locale.id);
locale.extensions.transform.clear();
locale.extensions.private.clear();
// b. Let availableLocale be ! BestAvailableLocale(availableLocales, noExtensionsLocale). // b. Let availableLocale be ! BestAvailableLocale(availableLocales, noExtensionsLocale).
let available_locale = best_available_locale::<M>(id, icu.provider()); let available_locale = best_available_locale::<M>(id, icu.provider());
@ -371,6 +373,8 @@ where
})) }))
{ {
let id = std::mem::take(&mut locale.id); let id = std::mem::take(&mut locale.id);
locale.extensions.transform.clear();
locale.extensions.private.clear();
if let Some(available) = best_locale_for_provider(id, icu.provider()) { if let Some(available) = best_locale_for_provider(id, icu.provider()) {
locale.id = available; locale.id = available;

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

@ -2,10 +2,12 @@ use std::fmt;
use crate::builtins::options::{ParsableOptionType, RoundingMode}; use crate::builtins::options::{ParsableOptionType, RoundingMode};
use super::RoundingIncrement;
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct DigitFormatOptions { pub(crate) struct DigitFormatOptions {
pub(crate) minimum_integer_digits: u8, pub(crate) minimum_integer_digits: u8,
pub(crate) rounding_increment: u16, pub(crate) rounding_increment: RoundingIncrement,
pub(crate) rounding_mode: RoundingMode, pub(crate) rounding_mode: RoundingMode,
pub(crate) trailing_zero_display: TrailingZeroDisplay, pub(crate) trailing_zero_display: TrailingZeroDisplay,
pub(crate) rounding_type: RoundingType, pub(crate) rounding_type: RoundingType,

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

@ -1,5 +1,5 @@
use boa_macros::utf16; use boa_macros::utf16;
use fixed_decimal::{FixedDecimal, FloatPrecision}; use fixed_decimal::{FixedDecimal, FloatPrecision, RoundingIncrement as BaseMultiple};
use crate::{ use crate::{
builtins::{ builtins::{
@ -14,6 +14,76 @@ use crate::{
use super::{DigitFormatOptions, Notation, RoundingPriority}; use super::{DigitFormatOptions, Notation, RoundingPriority};
/// The increment of a rounding operation.
///
/// This differs from [`fixed_decimal::RoundingIncrement`] because ECMA402 accepts
/// several more increments than `fixed_decimal`, but all increments can be decomposed
/// into the target multiple and the magnitude offset.
///
/// For example, rounding the number `0.02456` to the increment 200 at position
/// -3 is equivalent to rounding the same number to the increment 2 at position -1, and adding
/// trailing zeroes.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub(crate) struct RoundingIncrement {
multiple: BaseMultiple,
// INVARIANT: can only be 0, 1, 2, or 3
magnitude_offset: u8,
}
impl RoundingIncrement {
/// Creates a `RoundingIncrement` from the numeric value of the increment.
fn from_u16(increment: u16) -> Option<Self> {
let mut offset = 0u8;
let multiple = loop {
let rem = increment % 10u16.checked_pow(u32::from(offset + 1))?;
if rem != 0 {
break increment / 10u16.pow(u32::from(offset));
}
offset += 1;
};
if offset > 3 {
return None;
}
let multiple = match multiple {
1 => BaseMultiple::MultiplesOf1,
2 => BaseMultiple::MultiplesOf2,
5 => BaseMultiple::MultiplesOf5,
25 => BaseMultiple::MultiplesOf25,
_ => return None,
};
Some(RoundingIncrement {
multiple,
magnitude_offset: offset,
})
}
/// Gets the numeric value of this `RoundingIncrement`.
pub(crate) fn to_u16(self) -> u16 {
u16::from(self.magnitude_offset + 1)
* match self.multiple {
BaseMultiple::MultiplesOf1 => 1,
BaseMultiple::MultiplesOf2 => 2,
BaseMultiple::MultiplesOf5 => 5,
BaseMultiple::MultiplesOf25 => 25,
_ => {
debug_assert!(false, "base multiples can only be 1, 2, 5, or 25");
1
}
}
}
/// Gets the magnitude offset that needs to be added to the rounding position
/// for this rounding increment.
fn magnitude_offset(self) -> i16 {
i16::from(self.magnitude_offset)
}
}
/// Abstract operation [`SetNumberFormatDigitOptions ( intlObj, options, mnfdDefault, mxfdDefault, notation )`][spec]. /// 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. /// Gets the digit format options of the number formatter from the options object and the requested notation.
@ -26,10 +96,6 @@ pub(crate) fn get_digit_format_options(
notation: Notation, notation: Notation,
context: &mut Context, context: &mut Context,
) -> JsResult<DigitFormatOptions> { ) -> 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). // 1. Let mnid be ? GetNumberOption(options, "minimumIntegerDigits,", 1, 21, 1).
let minimum_integer_digits = let minimum_integer_digits =
get_number_option(options, utf16!("minimumIntegerDigits"), 1, 21, context)?.unwrap_or(1); get_number_option(options, utf16!("minimumIntegerDigits"), 1, 21, context)?.unwrap_or(1);
@ -51,11 +117,9 @@ pub(crate) fn get_digit_format_options(
let rounding_increment = let rounding_increment =
get_number_option(options, utf16!("roundingIncrement"), 1, 5000, context)?.unwrap_or(1); get_number_option(options, utf16!("roundingIncrement"), 1, 5000, context)?.unwrap_or(1);
if !VALID_ROUNDING_INCREMENTS.contains(&rounding_increment) { let rounding_increment = RoundingIncrement::from_u16(rounding_increment).ok_or_else(|| {
return Err(JsNativeError::range() JsNativeError::range().with_message("invalid value for option `roundingIncrement`")
.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"). // 10. Let roundingMode be ? GetOption(options, "roundingMode", string, « "ceil", "floor", "expand", "trunc", "halfCeil", "halfFloor", "halfExpand", "halfTrunc", "halfEven" », "halfExpand").
let rounding_mode = get_option(options, utf16!("roundingMode"), context)?.unwrap_or_default(); let rounding_mode = get_option(options, utf16!("roundingMode"), context)?.unwrap_or_default();
@ -67,7 +131,7 @@ pub(crate) fn get_digit_format_options(
// 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. // 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. // 13. If roundingIncrement is not 1, set mxfdDefault to mnfdDefault.
if rounding_increment != 1 { if rounding_increment.to_u16() != 1 {
max_float_digits_default = min_float_digits_default; max_float_digits_default = min_float_digits_default;
} }
@ -220,7 +284,7 @@ pub(crate) fn get_digit_format_options(
(None, Some(frac)) => RoundingType::FractionDigits(frac), (None, Some(frac)) => RoundingType::FractionDigits(frac),
}; };
if rounding_increment != 1 { if rounding_increment.to_u16() != 1 {
let RoundingType::FractionDigits(range) = rounding_type else { let RoundingType::FractionDigits(range) = rounding_type else {
return Err(JsNativeError::typ() return Err(JsNativeError::typ()
.with_message("option `roundingIncrement` invalid for the current set of options") .with_message("option `roundingIncrement` invalid for the current set of options")
@ -257,17 +321,17 @@ pub(crate) fn f64_to_formatted_fixed_decimal(
number: f64, number: f64,
options: &DigitFormatOptions, options: &DigitFormatOptions,
) -> FixedDecimal { ) -> FixedDecimal {
fn round(number: &mut FixedDecimal, position: i16, mode: RoundingMode) { fn round(number: &mut FixedDecimal, position: i16, mode: RoundingMode, multiple: BaseMultiple) {
match mode { match mode {
RoundingMode::Ceil => number.ceil(position), RoundingMode::Ceil => number.ceil_to_increment(position, multiple),
RoundingMode::Floor => number.floor(position), RoundingMode::Floor => number.floor_to_increment(position, multiple),
RoundingMode::Expand => number.expand(position), RoundingMode::Expand => number.expand_to_increment(position, multiple),
RoundingMode::Trunc => number.trunc(position), RoundingMode::Trunc => number.trunc_to_increment(position, multiple),
RoundingMode::HalfCeil => number.half_ceil(position), RoundingMode::HalfCeil => number.half_ceil_to_increment(position, multiple),
RoundingMode::HalfFloor => number.half_floor(position), RoundingMode::HalfFloor => number.half_floor_to_increment(position, multiple),
RoundingMode::HalfExpand => number.half_expand(position), RoundingMode::HalfExpand => number.half_expand_to_increment(position, multiple),
RoundingMode::HalfTrunc => number.half_trunc(position), RoundingMode::HalfTrunc => number.half_trunc_to_increment(position, multiple),
RoundingMode::HalfEven => number.half_even(position), RoundingMode::HalfEven => number.half_even_to_increment(position, multiple),
} }
} }
@ -282,7 +346,7 @@ pub(crate) fn f64_to_formatted_fixed_decimal(
let min_msb = msb - i16::from(min_precision) + 1; let min_msb = msb - i16::from(min_precision) + 1;
let max_msb = msb - i16::from(max_precision) + 1; let max_msb = msb - i16::from(max_precision) + 1;
number.pad_end(min_msb); number.pad_end(min_msb);
round(number, max_msb, rounding_mode); round(number, max_msb, rounding_mode, BaseMultiple::MultiplesOf1);
max_msb max_msb
} }
@ -291,12 +355,21 @@ pub(crate) fn f64_to_formatted_fixed_decimal(
number: &mut FixedDecimal, number: &mut FixedDecimal,
min_fraction: u8, min_fraction: u8,
max_fraction: u8, max_fraction: u8,
// TODO: missing support for `roundingIncrement` on `FixedDecimal`. rounding_increment: RoundingIncrement,
_rounding_increment: u16,
rounding_mode: RoundingMode, rounding_mode: RoundingMode,
) -> i16 { ) -> i16 {
#[cfg(debug_assertions)]
if rounding_increment.to_u16() != 1 {
assert_eq!(min_fraction, max_fraction);
}
number.pad_end(-i16::from(min_fraction)); number.pad_end(-i16::from(min_fraction));
round(number, -i16::from(max_fraction), rounding_mode); round(
number,
rounding_increment.magnitude_offset() - i16::from(max_fraction),
rounding_mode,
rounding_increment.multiple,
);
-i16::from(max_fraction) -i16::from(max_fraction)
} }
@ -405,3 +478,48 @@ pub(crate) fn f64_to_formatted_fixed_decimal(
// 14. Return the Record { [[RoundedNumber]]: x, [[FormattedString]]: string }. // 14. Return the Record { [[RoundedNumber]]: x, [[FormattedString]]: string }.
number number
} }
#[cfg(test)]
mod tests {
use crate::builtins::intl::number_format::RoundingIncrement;
use fixed_decimal::RoundingIncrement::*;
#[test]
fn u16_to_rounding_increment_sunny_day() {
#[rustfmt::skip]
const VALID_CASES: [(u16, RoundingIncrement); 15] = [
// Singles
(1, RoundingIncrement { multiple: MultiplesOf1, magnitude_offset: 0 }),
(2, RoundingIncrement { multiple: MultiplesOf2, magnitude_offset: 0 }),
(5, RoundingIncrement { multiple: MultiplesOf5, magnitude_offset: 0 }),
// Tens
(10, RoundingIncrement { multiple: MultiplesOf1, magnitude_offset: 1 }),
(20, RoundingIncrement { multiple: MultiplesOf2, magnitude_offset: 1 }),
(25, RoundingIncrement { multiple: MultiplesOf25, magnitude_offset: 0 }),
(50, RoundingIncrement { multiple: MultiplesOf5, magnitude_offset: 1 }),
// Hundreds
(100, RoundingIncrement { multiple: MultiplesOf1, magnitude_offset: 2 }),
(200, RoundingIncrement { multiple: MultiplesOf2, magnitude_offset: 2 }),
(250, RoundingIncrement { multiple: MultiplesOf25, magnitude_offset: 1 }),
(500, RoundingIncrement { multiple: MultiplesOf5, magnitude_offset: 2 }),
// Thousands
(1000, RoundingIncrement { multiple: MultiplesOf1, magnitude_offset: 3 }),
(2000, RoundingIncrement { multiple: MultiplesOf2, magnitude_offset: 3 }),
(2500, RoundingIncrement { multiple: MultiplesOf25, magnitude_offset: 2 }),
(5000, RoundingIncrement { multiple: MultiplesOf5, magnitude_offset: 3 }),
];
for (num, increment) in VALID_CASES {
assert_eq!(RoundingIncrement::from_u16(num), Some(increment));
}
}
#[test]
fn u16_to_rounding_increment_rainy_day() {
const INVALID_CASES: [u16; 9] = [0, 4, 6, 24, 10000, 65535, 7373, 140, 1500];
for num in INVALID_CASES {
assert!(RoundingIncrement::from_u16(num).is_none());
}
}
}

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

@ -6,6 +6,7 @@ use fixed_decimal::FixedDecimal;
use icu_locid::Locale; use icu_locid::Locale;
use icu_plurals::{ use icu_plurals::{
provider::CardinalV1Marker, PluralCategory, PluralRuleType, PluralRules as NativePluralRules, provider::CardinalV1Marker, PluralCategory, PluralRuleType, PluralRules as NativePluralRules,
PluralRulesWithRanges,
}; };
use icu_provider::DataLocale; use icu_provider::DataLocale;
@ -36,7 +37,7 @@ use super::{
#[derive(Debug)] #[derive(Debug)]
pub struct PluralRules { pub struct PluralRules {
locale: Locale, locale: Locale,
native: NativePluralRules, native: PluralRulesWithRanges<NativePluralRules>,
rule_type: PluralRuleType, rule_type: PluralRuleType,
format_options: DigitFormatOptions, format_options: DigitFormatOptions,
} }
@ -64,6 +65,7 @@ impl IntrinsicObject for PluralRules {
) )
.method(Self::resolved_options, js_string!("resolvedOptions"), 0) .method(Self::resolved_options, js_string!("resolvedOptions"), 0)
.method(Self::select, js_string!("select"), 1) .method(Self::select, js_string!("select"), 1)
.method(Self::select_range, js_string!("selectRange"), 2)
.build(); .build();
} }
@ -93,6 +95,12 @@ impl BuiltInConstructor for PluralRules {
.with_message("cannot call `Intl.PluralRules` constructor without `new`") .with_message("cannot call `Intl.PluralRules` constructor without `new`")
.into()); .into());
} }
let proto = get_prototype_from_constructor(
new_target,
StandardConstructors::plural_rules,
context,
)?;
// 2. Let pluralRules be ? OrdinaryCreateFromConstructor(NewTarget, "%PluralRules.prototype%", // 2. Let pluralRules be ? OrdinaryCreateFromConstructor(NewTarget, "%PluralRules.prototype%",
// « [[InitializedPluralRules]], [[Locale]], [[Type]], [[MinimumIntegerDigits]], // « [[InitializedPluralRules]], [[Locale]], [[Type]], [[MinimumIntegerDigits]],
// [[MinimumFractionDigits]], [[MaximumFractionDigits]], [[MinimumSignificantDigits]], // [[MinimumFractionDigits]], [[MaximumFractionDigits]], [[MinimumSignificantDigits]],
@ -136,11 +144,11 @@ impl BuiltInConstructor for PluralRules {
); );
let native = match rule_type { let native = match rule_type {
PluralRuleType::Cardinal => NativePluralRules::try_new_cardinal_unstable( PluralRuleType::Cardinal => PluralRulesWithRanges::try_new_cardinal_unstable(
context.icu().provider(), context.icu().provider(),
&DataLocale::from(&locale), &DataLocale::from(&locale),
), ),
PluralRuleType::Ordinal => NativePluralRules::try_new_ordinal_unstable( PluralRuleType::Ordinal => PluralRulesWithRanges::try_new_ordinal_unstable(
context.icu().provider(), context.icu().provider(),
&DataLocale::from(&locale), &DataLocale::from(&locale),
), ),
@ -152,12 +160,6 @@ impl BuiltInConstructor for PluralRules {
} }
.map_err(|e| JsNativeError::typ().with_message(e.to_string()))?; .map_err(|e| JsNativeError::typ().with_message(e.to_string()))?;
let proto = get_prototype_from_constructor(
new_target,
StandardConstructors::plural_rules,
context,
)?;
// 12. Return pluralRules. // 12. Return pluralRules.
Ok(JsObject::from_proto_and_data_with_shared_shape( Ok(JsObject::from_proto_and_data_with_shared_shape(
context.root_shape(), context.root_shape(),
@ -187,20 +189,86 @@ impl PluralRules {
// 1. Let pr be the this value. // 1. Let pr be the this value.
// 2. Perform ? RequireInternalSlot(pr, [[InitializedPluralRules]]). // 2. Perform ? RequireInternalSlot(pr, [[InitializedPluralRules]]).
let plural_rules = this.as_object().map(JsObject::borrow).ok_or_else(|| { let plural_rules = this.as_object().map(JsObject::borrow).ok_or_else(|| {
JsNativeError::typ().with_message( JsNativeError::typ()
"`resolved_options` can only be called on an `Intl.PluralRules` object", .with_message("`select` can only be called on an `Intl.PluralRules` object")
)
})?; })?;
let plural_rules = plural_rules.as_plural_rules().ok_or_else(|| { let plural_rules = plural_rules.as_plural_rules().ok_or_else(|| {
JsNativeError::typ().with_message( JsNativeError::typ()
"`resolved_options` can only be called on an `Intl.PluralRules` object", .with_message("`select` can only be called on an `Intl.PluralRules` object")
)
})?; })?;
let n = args.get_or_undefined(0).to_number(context)?; let n = args.get_or_undefined(0).to_number(context)?;
Ok(plural_category_to_js_string(resolve_plural(plural_rules, n).category).into()) Ok(plural_category_to_js_string(resolve_plural(plural_rules, n).category).into())
} }
/// [`Intl.PluralRules.prototype.selectRange ( start, end )`][spec].
///
/// Receives two values and returns a string indicating which plural rule to use for
/// locale-aware formatting of the indicated range.
///
/// More information:
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma402/#sec-intl.pluralrules.prototype.selectrange
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules/selectRange
fn select_range(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("`select_range` can only be called on an `Intl.PluralRules` object")
})?;
let plural_rules = plural_rules.as_plural_rules().ok_or_else(|| {
JsNativeError::typ()
.with_message("`select_range` can only be called on an `Intl.PluralRules` object")
})?;
// 3. If start is undefined or end is undefined, throw a TypeError exception.
let x = args.get_or_undefined(0);
let y = args.get_or_undefined(1);
if x.is_undefined() || y.is_undefined() {
return Err(JsNativeError::typ()
.with_message("extremum of range cannot be `undefined`")
.into());
}
// 4. Let x be ? ToNumber(start).
let x = x.to_number(context)?;
// 5. Let y be ? ToNumber(end).
let y = y.to_number(context)?;
// 6. Return ? ResolvePluralRange(pr, x, y).
// ResolvePluralRange(pr, x, y)
// <https://tc39.es/ecma402/#sec-resolvepluralrange>
// 1. If x is NaN or y is NaN, throw a RangeError exception.
if x.is_nan() || y.is_nan() {
return Err(JsNativeError::typ()
.with_message("extremum of range cannot be NaN")
.into());
}
// 2. Let xp be ResolvePlural(pluralRules, x).
let x = resolve_plural(plural_rules, x);
// 3. Let yp be ResolvePlural(pluralRules, y).
let y = resolve_plural(plural_rules, y);
// 4. If xp.[[FormattedString]] is yp.[[FormattedString]], then
if x.formatted == y.formatted {
// a. Return xp.[[PluralCategory]].
return Ok(plural_category_to_js_string(x.category).into());
}
// 5. Let locale be pluralRules.[[Locale]].
// 6. Let type be pluralRules.[[Type]].
// 7. Return PluralRuleSelectRange(locale, type, xp.[[PluralCategory]], yp.[[PluralCategory]]).
Ok(
plural_category_to_js_string(plural_rules.native.resolve_range(x.category, y.category))
.into(),
)
}
/// [`Intl.PluralRules.supportedLocalesOf ( locales [ , options ] )`][spec]. /// [`Intl.PluralRules.supportedLocalesOf ( locales [ , options ] )`][spec].
/// ///
/// Returns an array containing those of the provided locales that are supported in plural rules /// Returns an array containing those of the provided locales that are supported in plural rules
@ -322,7 +390,7 @@ impl PluralRules {
) )
.property( .property(
js_string!("roundingIncrement"), js_string!("roundingIncrement"),
plural_rules.format_options.rounding_increment, plural_rules.format_options.rounding_increment.to_u16(),
Attribute::all(), Attribute::all(),
) )
.property( .property(
@ -339,6 +407,7 @@ impl PluralRules {
let plural_categories = Array::create_array_from_list( let plural_categories = Array::create_array_from_list(
plural_rules plural_rules
.native .native
.rules()
.categories() .categories()
.map(|category| plural_category_to_js_string(category).into()), .map(|category| plural_category_to_js_string(category).into()),
options.context(), options.context(),
@ -401,7 +470,7 @@ fn resolve_plural(plural_rules: &PluralRules, n: f64) -> ResolvedPlural {
// 8. Let s be res.[[FormattedString]]. // 8. Let s be res.[[FormattedString]].
// 9. Let operands be ! GetOperands(s). // 9. Let operands be ! GetOperands(s).
// 10. Let p be ! PluralRuleSelect(locale, type, n, operands). // 10. Let p be ! PluralRuleSelect(locale, type, n, operands).
let category = plural_rules.native.category_for(&fixed); let category = plural_rules.native.rules().category_for(&fixed);
// 11. Return the Record { [[PluralCategory]]: p, [[FormattedString]]: s }. // 11. Return the Record { [[PluralCategory]]: p, [[FormattedString]]: s }.
ResolvedPlural { ResolvedPlural {

4
boa_engine/src/object/mod.rs

@ -438,7 +438,7 @@ pub enum ObjectKind {
SegmentIterator(SegmentIterator), SegmentIterator(SegmentIterator),
/// The `PluralRules` object kind. /// The `PluralRules` object kind.
#[cfg(feature = "intl")] #[cfg(feature = "intl")]
PluralRules(PluralRules), PluralRules(Box<PluralRules>),
/// The `Temporal.Instant` object kind. /// The `Temporal.Instant` object kind.
#[cfg(feature = "temporal")] #[cfg(feature = "temporal")]
@ -982,7 +982,7 @@ impl ObjectData {
#[must_use] #[must_use]
pub fn plural_rules(plural_rules: PluralRules) -> Self { pub fn plural_rules(plural_rules: PluralRules) -> Self {
Self { Self {
kind: ObjectKind::PluralRules(plural_rules), kind: ObjectKind::PluralRules(Box::new(plural_rules)),
internal_methods: &ORDINARY_INTERNAL_METHODS, internal_methods: &ORDINARY_INTERNAL_METHODS,
} }
} }

5
boa_icu_provider/Cargo.toml

@ -13,19 +13,20 @@ rust-version.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
icu_provider = { workspace = true, features = ["serde", "sync"] } icu_provider = { workspace = true, features = ["serde", "sync", "datagen"] }
icu_provider_blob = { workspace = true, features = ["export"] } icu_provider_blob = { workspace = true, features = ["export"] }
icu_provider_adapters = { workspace = true, features = ["serde"] } icu_provider_adapters = { workspace = true, features = ["serde"] }
once_cell = { workspace = true, default-features = false, features = ["critical-section"] } once_cell = { workspace = true, default-features = false, features = ["critical-section"] }
icu_datagen = { workspace = true, optional = true, features = ["networking", "use_wasm"] } icu_datagen = { workspace = true, optional = true, features = ["networking", "use_wasm"] }
icu_plurals = { workspace = true, optional = true, features = ["datagen", "experimental"] }
log = { version = "0.4.20", optional = true } log = { version = "0.4.20", optional = true }
simple_logger = { version = "4.2.0", optional = true } simple_logger = { version = "4.2.0", optional = true }
[features] [features]
default = ["std"] default = ["std"]
std = ["once_cell/std"] std = ["once_cell/std"]
bin = ["dep:icu_datagen", "dep:simple_logger", "dep:log"] bin = ["dep:icu_datagen", "dep:simple_logger", "dep:log", "dep:icu_plurals"]
[[bin]] [[bin]]
name = "boa_datagen" name = "boa_datagen"

BIN
boa_icu_provider/data/icudata.postcard

Binary file not shown.

72
boa_icu_provider/src/bin/datagen.rs

@ -12,8 +12,78 @@ use std::{error::Error, fs::File};
use boa_icu_provider::data_root; use boa_icu_provider::data_root;
use icu_datagen::{all_keys, CoverageLevel, DatagenDriver, DatagenProvider}; use icu_datagen::{all_keys, CoverageLevel, DatagenDriver, DatagenProvider};
use icu_plurals::provider::{PluralRangesV1, PluralRangesV1Marker};
use icu_provider::{
datagen::{ExportMarker, IterableDynamicDataProvider},
dynutil::UpcastDataPayload,
prelude::*,
};
use icu_provider_blob::export::BlobExporter; use icu_provider_blob::export::BlobExporter;
/// Hack that associates the `und` locale with an empty plural ranges data.
/// This enables the default behaviour for all locales without data.
#[derive(Debug)]
struct PluralRangesFallbackHack(DatagenProvider);
// We definitely don't want to import dependencies just to do `T::default`.
#[allow(clippy::default_trait_access)]
impl DynamicDataProvider<AnyMarker> for PluralRangesFallbackHack {
fn load_data(
&self,
key: DataKey,
req: DataRequest<'_>,
) -> Result<DataResponse<AnyMarker>, DataError> {
if req.locale.is_und() && key.hashed() == PluralRangesV1Marker::KEY.hashed() {
let payload = <AnyMarker as UpcastDataPayload<PluralRangesV1Marker>>::upcast(
DataPayload::from_owned(PluralRangesV1 {
ranges: Default::default(),
}),
);
Ok(DataResponse {
metadata: DataResponseMetadata::default(),
payload: Some(payload),
})
} else {
self.0.load_data(key, req)
}
}
}
#[allow(clippy::default_trait_access)]
impl DynamicDataProvider<ExportMarker> for PluralRangesFallbackHack {
fn load_data(
&self,
key: DataKey,
req: DataRequest<'_>,
) -> Result<DataResponse<ExportMarker>, DataError> {
if req.locale.is_und() && key.hashed() == PluralRangesV1Marker::KEY.hashed() {
let payload = <ExportMarker as UpcastDataPayload<PluralRangesV1Marker>>::upcast(
DataPayload::from_owned(PluralRangesV1 {
ranges: Default::default(),
}),
);
Ok(DataResponse {
metadata: DataResponseMetadata::default(),
payload: Some(payload),
})
} else {
self.0.load_data(key, req)
}
}
}
impl IterableDynamicDataProvider<ExportMarker> for PluralRangesFallbackHack {
fn supported_locales_for_key(&self, key: DataKey) -> Result<Vec<DataLocale>, DataError> {
if key.hashed() == PluralRangesV1Marker::KEY.hashed() {
let mut locales = self.0.supported_locales_for_key(key)?;
locales.push(DataLocale::default());
Ok(locales)
} else {
self.0.supported_locales_for_key(key)
}
}
}
fn main() -> Result<(), Box<dyn Error>> { fn main() -> Result<(), Box<dyn Error>> {
simple_logger::SimpleLogger::new() simple_logger::SimpleLogger::new()
.env() .env()
@ -27,7 +97,7 @@ fn main() -> Result<(), Box<dyn Error>> {
.with_locales(provider.locales_for_coverage_levels([CoverageLevel::Modern])?) .with_locales(provider.locales_for_coverage_levels([CoverageLevel::Modern])?)
.with_additional_collations([String::from("search*")]) .with_additional_collations([String::from("search*")])
.export( .export(
&provider, &PluralRangesFallbackHack(provider),
BlobExporter::new_with_sink(Box::new(File::create( BlobExporter::new_with_sink(Box::new(File::create(
data_root().join("icudata.postcard"), data_root().join("icudata.postcard"),
)?)), )?)),

Loading…
Cancel
Save