mirror of https://github.com/boa-dev/boa.git
Browse Source
* Implement `PluralRules` * cargo fmt * Move options utils to builtins module * Fix docs * Apply reviewpull/3300/head
José Julián Espina
1 year ago
committed by
GitHub
25 changed files with 1377 additions and 173 deletions
@ -0,0 +1,4 @@
|
||||
mod options; |
||||
mod utils; |
||||
pub(crate) use options::*; |
||||
pub(crate) use utils::*; |
@ -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, |
||||
} |
||||
} |
||||
} |
@ -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 |
||||
} |
@ -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"), |
||||
} |
||||
} |
@ -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()), |
||||
} |
||||
} |
||||
} |
@ -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) |
||||
} |
||||
} |
Loading…
Reference in new issue