mirror of https://github.com/boa-dev/boa.git
Browse Source
* Implement prototype of `NumberFormat` * Fix clippy * Misc fixes * Apply reviewpull/3671/head
José Julián Espina
10 months ago
committed by
GitHub
19 changed files with 2109 additions and 616 deletions
@ -1,4 +1,851 @@ |
|||||||
|
use std::borrow::Cow; |
||||||
|
|
||||||
|
use boa_gc::{Finalize, Trace}; |
||||||
|
use boa_macros::utf16; |
||||||
|
use boa_profiler::Profiler; |
||||||
|
use fixed_decimal::{FixedDecimal, FloatPrecision, SignDisplay}; |
||||||
|
use icu_decimal::{ |
||||||
|
options::{FixedDecimalFormatterOptions, GroupingStrategy}, |
||||||
|
provider::DecimalSymbolsV1Marker, |
||||||
|
FixedDecimalFormatter, FormattedFixedDecimal, |
||||||
|
}; |
||||||
|
|
||||||
mod options; |
mod options; |
||||||
mod utils; |
use icu_locid::{ |
||||||
|
extensions::unicode::{key, Value}, |
||||||
|
Locale, |
||||||
|
}; |
||||||
|
use num_bigint::BigInt; |
||||||
|
use num_traits::Num; |
||||||
pub(crate) use options::*; |
pub(crate) use options::*; |
||||||
pub(crate) use utils::*; |
|
||||||
|
use crate::{ |
||||||
|
builtins::{ |
||||||
|
builder::BuiltInBuilder, options::get_option, string::is_trimmable_whitespace, |
||||||
|
BuiltInConstructor, BuiltInObject, IntrinsicObject, |
||||||
|
}, |
||||||
|
context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, |
||||||
|
js_string, |
||||||
|
object::{ |
||||||
|
internal_methods::get_prototype_from_constructor, FunctionObjectBuilder, JsFunction, |
||||||
|
ObjectInitializer, |
||||||
|
}, |
||||||
|
property::{Attribute, PropertyDescriptor}, |
||||||
|
realm::Realm, |
||||||
|
string::common::StaticJsStrings, |
||||||
|
value::PreferredType, |
||||||
|
Context, JsArgs, JsData, JsNativeError, JsObject, JsResult, JsString, JsSymbol, JsValue, |
||||||
|
NativeFunction, |
||||||
|
}; |
||||||
|
|
||||||
|
use super::{ |
||||||
|
locale::{canonicalize_locale_list, resolve_locale, supported_locales, validate_extension}, |
||||||
|
options::{coerce_options_to_object, IntlOptions}, |
||||||
|
Service, |
||||||
|
}; |
||||||
|
|
||||||
|
#[cfg(test)] |
||||||
|
mod tests; |
||||||
|
|
||||||
|
#[derive(Debug, Trace, Finalize, JsData)] |
||||||
|
// Safety: `NumberFormat` only contains non-traceable types.
|
||||||
|
#[boa_gc(unsafe_empty_trace)] |
||||||
|
pub(crate) struct NumberFormat { |
||||||
|
locale: Locale, |
||||||
|
formatter: FixedDecimalFormatter, |
||||||
|
numbering_system: Option<Value>, |
||||||
|
unit_options: UnitFormatOptions, |
||||||
|
digit_options: DigitFormatOptions, |
||||||
|
notation: Notation, |
||||||
|
use_grouping: GroupingStrategy, |
||||||
|
sign_display: SignDisplay, |
||||||
|
bound_format: Option<JsFunction>, |
||||||
|
} |
||||||
|
|
||||||
|
impl NumberFormat { |
||||||
|
/// [`FormatNumeric ( numberFormat, x )`][full] and [`FormatNumericToParts ( numberFormat, x )`][parts].
|
||||||
|
///
|
||||||
|
/// The returned struct implements `Writable`, allowing to either write the number as a full
|
||||||
|
/// string or by parts.
|
||||||
|
///
|
||||||
|
/// [full]: https://tc39.es/ecma402/#sec-formatnumber
|
||||||
|
/// [parts]: https://tc39.es/ecma402/#sec-formatnumbertoparts
|
||||||
|
fn format<'a>(&'a self, value: &'a mut FixedDecimal) -> FormattedFixedDecimal<'a> { |
||||||
|
// TODO: Missing support from ICU4X for Percent/Currency/Unit formatting.
|
||||||
|
// TODO: Missing support from ICU4X for Scientific/Engineering/Compact notation.
|
||||||
|
|
||||||
|
self.digit_options.format_fixed_decimal(value); |
||||||
|
value.apply_sign_display(self.sign_display); |
||||||
|
|
||||||
|
self.formatter.format(value) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[derive(Debug, Clone)] |
||||||
|
pub(super) struct NumberFormatLocaleOptions { |
||||||
|
numbering_system: Option<Value>, |
||||||
|
} |
||||||
|
|
||||||
|
impl Service for NumberFormat { |
||||||
|
type LangMarker = DecimalSymbolsV1Marker; |
||||||
|
|
||||||
|
type LocaleOptions = NumberFormatLocaleOptions; |
||||||
|
|
||||||
|
fn resolve( |
||||||
|
locale: &mut Locale, |
||||||
|
options: &mut Self::LocaleOptions, |
||||||
|
provider: &crate::context::icu::IntlProvider, |
||||||
|
) { |
||||||
|
let numbering_system = options |
||||||
|
.numbering_system |
||||||
|
.take() |
||||||
|
.filter(|nu| { |
||||||
|
validate_extension::<Self::LangMarker>(locale.id.clone(), key!("nu"), nu, provider) |
||||||
|
}) |
||||||
|
.or_else(|| { |
||||||
|
locale |
||||||
|
.extensions |
||||||
|
.unicode |
||||||
|
.keywords |
||||||
|
.get(&key!("nu")) |
||||||
|
.cloned() |
||||||
|
.filter(|nu| { |
||||||
|
validate_extension::<Self::LangMarker>( |
||||||
|
locale.id.clone(), |
||||||
|
key!("nu"), |
||||||
|
nu, |
||||||
|
provider, |
||||||
|
) |
||||||
|
}) |
||||||
|
}); |
||||||
|
|
||||||
|
locale.extensions.unicode.clear(); |
||||||
|
|
||||||
|
if let Some(nu) = numbering_system.clone() { |
||||||
|
locale.extensions.unicode.keywords.set(key!("nu"), nu); |
||||||
|
} |
||||||
|
|
||||||
|
options.numbering_system = numbering_system; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl IntrinsicObject for NumberFormat { |
||||||
|
fn init(realm: &Realm) { |
||||||
|
let _timer = Profiler::global().start_event(std::any::type_name::<Self>(), "init"); |
||||||
|
|
||||||
|
let get_format = BuiltInBuilder::callable(realm, Self::get_format) |
||||||
|
.name(js_string!("get format")) |
||||||
|
.build(); |
||||||
|
|
||||||
|
BuiltInBuilder::from_standard_constructor::<Self>(realm) |
||||||
|
.static_method( |
||||||
|
Self::supported_locales_of, |
||||||
|
js_string!("supportedLocalesOf"), |
||||||
|
1, |
||||||
|
) |
||||||
|
.property( |
||||||
|
JsSymbol::to_string_tag(), |
||||||
|
js_string!("Intl.NumberFormat"), |
||||||
|
Attribute::CONFIGURABLE, |
||||||
|
) |
||||||
|
.accessor( |
||||||
|
js_string!("format"), |
||||||
|
Some(get_format), |
||||||
|
None, |
||||||
|
Attribute::CONFIGURABLE, |
||||||
|
) |
||||||
|
.method(Self::resolved_options, js_string!("resolvedOptions"), 0) |
||||||
|
.build(); |
||||||
|
} |
||||||
|
|
||||||
|
fn get(intrinsics: &Intrinsics) -> JsObject { |
||||||
|
Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl BuiltInObject for NumberFormat { |
||||||
|
const NAME: JsString = StaticJsStrings::NUMBER_FORMAT; |
||||||
|
} |
||||||
|
|
||||||
|
impl BuiltInConstructor for NumberFormat { |
||||||
|
const LENGTH: usize = 0; |
||||||
|
|
||||||
|
const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor = |
||||||
|
StandardConstructors::number_format; |
||||||
|
|
||||||
|
/// [`Intl.NumberFormat ( [ locales [ , options ] ] )`][spec].
|
||||||
|
///
|
||||||
|
/// [spec]: https://tc39.es/ecma402/#sec-intl.numberformat
|
||||||
|
fn constructor( |
||||||
|
new_target: &JsValue, |
||||||
|
args: &[JsValue], |
||||||
|
context: &mut Context, |
||||||
|
) -> JsResult<JsValue> { |
||||||
|
let locales = args.get_or_undefined(0); |
||||||
|
let options = args.get_or_undefined(1); |
||||||
|
|
||||||
|
// 1. If NewTarget is undefined, let newTarget be the active function object, else let newTarget be NewTarget.
|
||||||
|
let new_target_inner = &if new_target.is_undefined() { |
||||||
|
context |
||||||
|
.active_function_object() |
||||||
|
.unwrap_or_else(|| { |
||||||
|
context |
||||||
|
.intrinsics() |
||||||
|
.constructors() |
||||||
|
.number_format() |
||||||
|
.constructor() |
||||||
|
}) |
||||||
|
.into() |
||||||
|
} else { |
||||||
|
new_target.clone() |
||||||
|
}; |
||||||
|
|
||||||
|
// 2. Let numberFormat be ? OrdinaryCreateFromConstructor(newTarget, "%Intl.NumberFormat.prototype%", « [[InitializedNumberFormat]], [[Locale]], [[DataLocale]], [[NumberingSystem]], [[Style]], [[Unit]], [[UnitDisplay]], [[Currency]], [[CurrencyDisplay]], [[CurrencySign]], [[MinimumIntegerDigits]], [[MinimumFractionDigits]], [[MaximumFractionDigits]], [[MinimumSignificantDigits]], [[MaximumSignificantDigits]], [[RoundingType]], [[Notation]], [[CompactDisplay]], [[UseGrouping]], [[SignDisplay]], [[RoundingIncrement]], [[RoundingMode]], [[ComputedRoundingPriority]], [[TrailingZeroDisplay]], [[BoundFormat]] »).
|
||||||
|
let prototype = get_prototype_from_constructor( |
||||||
|
new_target_inner, |
||||||
|
StandardConstructors::number_format, |
||||||
|
context, |
||||||
|
)?; |
||||||
|
|
||||||
|
// 3. Perform ? InitializeNumberFormat(numberFormat, locales, options).
|
||||||
|
|
||||||
|
// `InitializeNumberFormat ( numberFormat, locales, options )`
|
||||||
|
// https://tc39.es/ecma402/#sec-initializenumberformat
|
||||||
|
|
||||||
|
// 1. Let requestedLocales be ? CanonicalizeLocaleList(locales).
|
||||||
|
let requested_locales = canonicalize_locale_list(locales, context)?; |
||||||
|
// 2. Set options to ? CoerceOptionsToObject(options).
|
||||||
|
let options = coerce_options_to_object(options, context)?; |
||||||
|
|
||||||
|
// 3. Let opt be a new Record.
|
||||||
|
|
||||||
|
// 4. Let matcher be ? GetOption(options, "localeMatcher", string, « "lookup", "best fit" », "best fit").
|
||||||
|
// 5. Set opt.[[localeMatcher]] to matcher.
|
||||||
|
let matcher = get_option(&options, utf16!("localeMatcher"), context)?.unwrap_or_default(); |
||||||
|
|
||||||
|
// 6. Let numberingSystem be ? GetOption(options, "numberingSystem", string, empty, undefined).
|
||||||
|
// 7. If numberingSystem is not undefined, then
|
||||||
|
// a. If numberingSystem cannot be matched by the type Unicode locale nonterminal, throw a RangeError exception.
|
||||||
|
// 8. Set opt.[[nu]] to numberingSystem.
|
||||||
|
let numbering_system = get_option(&options, utf16!("numberingSystem"), context)?; |
||||||
|
|
||||||
|
let mut intl_options = IntlOptions { |
||||||
|
matcher, |
||||||
|
service_options: NumberFormatLocaleOptions { numbering_system }, |
||||||
|
}; |
||||||
|
|
||||||
|
// 9. Let localeData be %Intl.NumberFormat%.[[LocaleData]].
|
||||||
|
// 10. Let r be ResolveLocale(%Intl.NumberFormat%.[[AvailableLocales]], requestedLocales, opt, %Intl.NumberFormat%.[[RelevantExtensionKeys]], localeData).
|
||||||
|
let locale = resolve_locale::<Self>( |
||||||
|
&requested_locales, |
||||||
|
&mut intl_options, |
||||||
|
context.intl_provider(), |
||||||
|
); |
||||||
|
|
||||||
|
// 11. Set numberFormat.[[Locale]] to r.[[locale]].
|
||||||
|
// 12. Set numberFormat.[[DataLocale]] to r.[[dataLocale]].
|
||||||
|
// 13. Set numberFormat.[[NumberingSystem]] to r.[[nu]].
|
||||||
|
|
||||||
|
// 14. Perform ? SetNumberFormatUnitOptions(numberFormat, options).
|
||||||
|
let unit_options = UnitFormatOptions::from_options(&options, context)?; |
||||||
|
|
||||||
|
// 15. Let style be numberFormat.[[Style]].
|
||||||
|
// 16. If style is "currency", then
|
||||||
|
let (min_fractional, max_fractional) = if unit_options.style() == Style::Currency { |
||||||
|
// TODO: Missing support from ICU4X
|
||||||
|
// a. Let currency be numberFormat.[[Currency]].
|
||||||
|
// b. Let cDigits be CurrencyDigits(currency).
|
||||||
|
// c. Let mnfdDefault be cDigits.
|
||||||
|
// d. Let mxfdDefault be cDigits.
|
||||||
|
return Err(JsNativeError::typ().with_message("unimplemented").into()); |
||||||
|
} else { |
||||||
|
// 17. Else,
|
||||||
|
( |
||||||
|
// a. Let mnfdDefault be 0.
|
||||||
|
0, |
||||||
|
// b. If style is "percent", then
|
||||||
|
if unit_options.style() == Style::Percent { |
||||||
|
// i. Let mxfdDefault be 0.
|
||||||
|
0 |
||||||
|
} else { |
||||||
|
// c. Else,
|
||||||
|
// i. Let mxfdDefault be 3.
|
||||||
|
3 |
||||||
|
}, |
||||||
|
) |
||||||
|
}; |
||||||
|
|
||||||
|
// 18. Let notation be ? GetOption(options, "notation", string, « "standard", "scientific", "engineering", "compact" », "standard").
|
||||||
|
// 19. Set numberFormat.[[Notation]] to notation.
|
||||||
|
let notation = get_option(&options, utf16!("notation"), context)?.unwrap_or_default(); |
||||||
|
|
||||||
|
// 20. Perform ? SetNumberFormatDigitOptions(numberFormat, options, mnfdDefault, mxfdDefault, notation).
|
||||||
|
let digit_options = DigitFormatOptions::from_options( |
||||||
|
&options, |
||||||
|
min_fractional, |
||||||
|
max_fractional, |
||||||
|
notation, |
||||||
|
context, |
||||||
|
)?; |
||||||
|
|
||||||
|
// 21. Let compactDisplay be ? GetOption(options, "compactDisplay", string, « "short", "long" », "short").
|
||||||
|
let compact_display = |
||||||
|
get_option(&options, utf16!("compactDisplay"), context)?.unwrap_or_default(); |
||||||
|
|
||||||
|
// 22. Let defaultUseGrouping be "auto".
|
||||||
|
let mut default_use_grouping = GroupingStrategy::Auto; |
||||||
|
|
||||||
|
let notation = match notation { |
||||||
|
NotationKind::Standard => Notation::Standard, |
||||||
|
NotationKind::Scientific => Notation::Scientific, |
||||||
|
NotationKind::Engineering => Notation::Engineering, |
||||||
|
// 23. If notation is "compact", then
|
||||||
|
NotationKind::Compact => { |
||||||
|
// b. Set defaultUseGrouping to "min2".
|
||||||
|
default_use_grouping = GroupingStrategy::Min2; |
||||||
|
|
||||||
|
// a. Set numberFormat.[[CompactDisplay]] to compactDisplay.
|
||||||
|
Notation::Compact { |
||||||
|
display: compact_display, |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
// 24. NOTE: For historical reasons, the strings "true" and "false" are accepted and replaced with the default value.
|
||||||
|
// 25. Let useGrouping be ? GetBooleanOrStringNumberFormatOption(options, "useGrouping",
|
||||||
|
// « "min2", "auto", "always", "true", "false" », defaultUseGrouping).
|
||||||
|
// 26. If useGrouping is "true" or useGrouping is "false", set useGrouping to defaultUseGrouping.
|
||||||
|
// 27. If useGrouping is true, set useGrouping to "always".
|
||||||
|
// 28. Set numberFormat.[[UseGrouping]] to useGrouping.
|
||||||
|
// useGrouping requires special handling because of the "true" and "false" exceptions.
|
||||||
|
// We could also modify the `OptionType` interface but it complicates it a lot just for
|
||||||
|
// a single exception.
|
||||||
|
let use_grouping = 'block: { |
||||||
|
// GetBooleanOrStringNumberFormatOption ( options, property, stringValues, fallback )
|
||||||
|
// <https://tc39.es/ecma402/#sec-getbooleanorstringnumberformatoption>
|
||||||
|
|
||||||
|
// 1. Let value be ? Get(options, property).
|
||||||
|
let value = options.get(utf16!("useGrouping"), context)?; |
||||||
|
|
||||||
|
// 2. If value is undefined, return fallback.
|
||||||
|
if value.is_undefined() { |
||||||
|
break 'block default_use_grouping; |
||||||
|
} |
||||||
|
// 3. If value is true, return true.
|
||||||
|
if let &JsValue::Boolean(true) = &value { |
||||||
|
break 'block GroupingStrategy::Always; |
||||||
|
} |
||||||
|
|
||||||
|
// 4. If ToBoolean(value) is false, return false.
|
||||||
|
if !value.to_boolean() { |
||||||
|
break 'block GroupingStrategy::Never; |
||||||
|
} |
||||||
|
|
||||||
|
// 5. Set value to ? ToString(value).
|
||||||
|
// 6. If stringValues does not contain value, throw a RangeError exception.
|
||||||
|
// 7. Return value.
|
||||||
|
match value.to_string(context)?.to_std_string_escaped().as_str() { |
||||||
|
"min2" => GroupingStrategy::Min2, |
||||||
|
"auto" => GroupingStrategy::Auto, |
||||||
|
"always" => GroupingStrategy::Always, |
||||||
|
// special handling for historical reasons
|
||||||
|
"true" | "false" => default_use_grouping, |
||||||
|
_ => { |
||||||
|
return Err(JsNativeError::range() |
||||||
|
.with_message( |
||||||
|
"expected one of `min2`, `auto`, `always`, `true`, or `false`", |
||||||
|
) |
||||||
|
.into()) |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
// 29. Let signDisplay be ? GetOption(options, "signDisplay", string, « "auto", "never", "always", "exceptZero", "negative" », "auto").
|
||||||
|
// 30. Set numberFormat.[[SignDisplay]] to signDisplay.
|
||||||
|
let sign_display = |
||||||
|
get_option(&options, utf16!("signDisplay"), context)?.unwrap_or(SignDisplay::Auto); |
||||||
|
|
||||||
|
let formatter = FixedDecimalFormatter::try_new_unstable( |
||||||
|
context.intl_provider(), |
||||||
|
&locale.clone().into(), |
||||||
|
{ |
||||||
|
let mut options = FixedDecimalFormatterOptions::default(); |
||||||
|
options.grouping_strategy = use_grouping; |
||||||
|
options |
||||||
|
}, |
||||||
|
) |
||||||
|
.map_err(|err| JsNativeError::typ().with_message(err.to_string()))?; |
||||||
|
|
||||||
|
let number_format = JsObject::from_proto_and_data_with_shared_shape( |
||||||
|
context.root_shape(), |
||||||
|
prototype, |
||||||
|
NumberFormat { |
||||||
|
locale, |
||||||
|
numbering_system: intl_options.service_options.numbering_system, |
||||||
|
formatter, |
||||||
|
unit_options, |
||||||
|
digit_options, |
||||||
|
notation, |
||||||
|
use_grouping, |
||||||
|
sign_display, |
||||||
|
bound_format: None, |
||||||
|
}, |
||||||
|
); |
||||||
|
|
||||||
|
// 31. Return unused.
|
||||||
|
|
||||||
|
// 4. If the implementation supports the normative optional constructor mode of 4.3 Note 1, then
|
||||||
|
// a. Let this be the this value.
|
||||||
|
// b. Return ? ChainNumberFormat(numberFormat, NewTarget, this).
|
||||||
|
// ChainNumberFormat ( numberFormat, newTarget, this )
|
||||||
|
// <https://tc39.es/ecma402/#sec-chainnumberformat>
|
||||||
|
|
||||||
|
let this = context.vm.frame().this(&context.vm); |
||||||
|
let Some(this_obj) = this.as_object() else { |
||||||
|
return Ok(number_format.into()); |
||||||
|
}; |
||||||
|
|
||||||
|
let constructor = context |
||||||
|
.intrinsics() |
||||||
|
.constructors() |
||||||
|
.number_format() |
||||||
|
.constructor(); |
||||||
|
|
||||||
|
// 1. If newTarget is undefined and ? OrdinaryHasInstance(%Intl.NumberFormat%, this) is true, then
|
||||||
|
if new_target.is_undefined() |
||||||
|
&& JsValue::ordinary_has_instance(&constructor.into(), &this, context)? |
||||||
|
{ |
||||||
|
let fallback_symbol = context |
||||||
|
.intrinsics() |
||||||
|
.objects() |
||||||
|
.intl() |
||||||
|
.borrow() |
||||||
|
.data |
||||||
|
.fallback_symbol(); |
||||||
|
|
||||||
|
// a. Perform ? DefinePropertyOrThrow(this, %Intl%.[[FallbackSymbol]], PropertyDescriptor{ [[Value]]: numberFormat, [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: false }).
|
||||||
|
this_obj.define_property_or_throw( |
||||||
|
fallback_symbol, |
||||||
|
PropertyDescriptor::builder() |
||||||
|
.value(number_format) |
||||||
|
.writable(false) |
||||||
|
.enumerable(false) |
||||||
|
.configurable(false), |
||||||
|
context, |
||||||
|
)?; |
||||||
|
// b. Return this.
|
||||||
|
Ok(this) |
||||||
|
} else { |
||||||
|
// 2. Return numberFormat.
|
||||||
|
Ok(number_format.into()) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl NumberFormat { |
||||||
|
/// [`Intl.NumberFormat.supportedLocalesOf ( locales [ , options ] )`][spec].
|
||||||
|
///
|
||||||
|
/// Returns an array containing those of the provided locales that are supported in number format
|
||||||
|
/// without having to fall back to the runtime's default locale.
|
||||||
|
///
|
||||||
|
/// More information:
|
||||||
|
/// - [MDN documentation][mdn]
|
||||||
|
///
|
||||||
|
/// [spec]: https://tc39.es/ecma402/#sec-intl.numberformat.supportedlocalesof
|
||||||
|
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/supportedLocalesOf
|
||||||
|
fn supported_locales_of( |
||||||
|
_: &JsValue, |
||||||
|
args: &[JsValue], |
||||||
|
context: &mut Context, |
||||||
|
) -> JsResult<JsValue> { |
||||||
|
let locales = args.get_or_undefined(0); |
||||||
|
let options = args.get_or_undefined(1); |
||||||
|
|
||||||
|
// 1. Let availableLocales be %Intl.NumberFormat%.[[AvailableLocales]].
|
||||||
|
// 2. Let requestedLocales be ? CanonicalizeLocaleList(locales).
|
||||||
|
let requested_locales = canonicalize_locale_list(locales, context)?; |
||||||
|
|
||||||
|
// 3. Return ? SupportedLocales(availableLocales, requestedLocales, options).
|
||||||
|
supported_locales::<<Self as Service>::LangMarker>(&requested_locales, options, context) |
||||||
|
.map(JsValue::from) |
||||||
|
} |
||||||
|
|
||||||
|
/// [`get Intl.NumberFormat.prototype.format`][spec].
|
||||||
|
///
|
||||||
|
/// [spec]: https://tc39.es/ecma402/#sec-intl.numberformat.prototype.format
|
||||||
|
fn get_format(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> { |
||||||
|
// 1. Let nf be the this value.
|
||||||
|
// 2. If the implementation supports the normative optional constructor mode of 4.3 Note 1, then
|
||||||
|
// a. Set nf to ? UnwrapNumberFormat(nf).
|
||||||
|
// 3. Perform ? RequireInternalSlot(nf, [[InitializedNumberFormat]]).
|
||||||
|
let nf = unwrap_number_format(this, context)?; |
||||||
|
let nf_clone = nf.clone(); |
||||||
|
let mut nf = nf.borrow_mut(); |
||||||
|
|
||||||
|
let bound_format = if let Some(f) = nf.data.bound_format.clone() { |
||||||
|
f |
||||||
|
} else { |
||||||
|
// 4. If nf.[[BoundFormat]] is undefined, then
|
||||||
|
// a. Let F be a new built-in function object as defined in Number Format Functions (15.5.2).
|
||||||
|
// b. Set F.[[NumberFormat]] to nf.
|
||||||
|
// c. Set nf.[[BoundFormat]] to F.
|
||||||
|
let bound_format = FunctionObjectBuilder::new( |
||||||
|
context.realm(), |
||||||
|
// Number Format Functions
|
||||||
|
// <https://tc39.es/ecma402/#sec-number-format-functions>
|
||||||
|
NativeFunction::from_copy_closure_with_captures( |
||||||
|
|_, args, nf, context| { |
||||||
|
// 1. Let nf be F.[[NumberFormat]].
|
||||||
|
// 2. Assert: Type(nf) is Object and nf has an [[InitializedNumberFormat]] internal slot.
|
||||||
|
|
||||||
|
// 3. If value is not provided, let value be undefined.
|
||||||
|
let value = args.get_or_undefined(0); |
||||||
|
|
||||||
|
// 4. Let x be ? ToIntlMathematicalValue(value).
|
||||||
|
let mut x = to_intl_mathematical_value(value, context)?; |
||||||
|
|
||||||
|
// 5. Return FormatNumeric(nf, x).
|
||||||
|
Ok(js_string!(nf.borrow().data.format(&mut x).to_string()).into()) |
||||||
|
}, |
||||||
|
nf_clone, |
||||||
|
), |
||||||
|
) |
||||||
|
.length(2) |
||||||
|
.build(); |
||||||
|
|
||||||
|
nf.data.bound_format = Some(bound_format.clone()); |
||||||
|
bound_format |
||||||
|
}; |
||||||
|
|
||||||
|
// 5. Return nf.[[BoundFormat]].
|
||||||
|
Ok(bound_format.into()) |
||||||
|
} |
||||||
|
|
||||||
|
/// [`Intl.NumberFormat.prototype.resolvedOptions ( )`][spec].
|
||||||
|
///
|
||||||
|
/// Returns a new object with properties reflecting the locale and options computed during the
|
||||||
|
/// construction of the current `Intl.NumberFormat` object.
|
||||||
|
///
|
||||||
|
/// More information:
|
||||||
|
/// - [MDN documentation][mdn]
|
||||||
|
///
|
||||||
|
/// [spec]: https://tc39.es/ecma402/#sec-intl.numberformat.prototype.resolvedoptions
|
||||||
|
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/resolvedOptions
|
||||||
|
fn resolved_options(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> { |
||||||
|
// This function provides access to the locale and options computed during initialization of the object.
|
||||||
|
|
||||||
|
// 1. Let nf be the this value.
|
||||||
|
// 2. If the implementation supports the normative optional constructor mode of 4.3 Note 1, then
|
||||||
|
// a. Set nf to ? UnwrapNumberFormat(nf).
|
||||||
|
// 3. Perform ? RequireInternalSlot(nf, [[InitializedNumberFormat]]).
|
||||||
|
let nf = unwrap_number_format(this, context)?; |
||||||
|
let nf = nf.borrow(); |
||||||
|
let nf = &nf.data; |
||||||
|
|
||||||
|
// 4. Let options be OrdinaryObjectCreate(%Object.prototype%).
|
||||||
|
// 5. For each row of Table 12, except the header row, in table order, do
|
||||||
|
// a. Let p be the Property value of the current row.
|
||||||
|
// b. Let v be the value of nf's internal slot whose name is the Internal Slot value of the current row.
|
||||||
|
// c. If v is not undefined, then
|
||||||
|
// i. If there is a Conversion value in the current row, then
|
||||||
|
// 1. Assert: The Conversion value of the current row is number.
|
||||||
|
// 2. Set v to 𝔽(v).
|
||||||
|
// ii. Perform ! CreateDataPropertyOrThrow(options, p, v).
|
||||||
|
let mut options = ObjectInitializer::new(context); |
||||||
|
options.property( |
||||||
|
js_string!("locale"), |
||||||
|
js_string!(nf.locale.to_string()), |
||||||
|
Attribute::all(), |
||||||
|
); |
||||||
|
if let Some(nu) = &nf.numbering_system { |
||||||
|
options.property( |
||||||
|
js_string!("numberingSystem"), |
||||||
|
js_string!(nu.to_string()), |
||||||
|
Attribute::all(), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
options.property( |
||||||
|
js_string!("style"), |
||||||
|
nf.unit_options.style().to_js_string(), |
||||||
|
Attribute::all(), |
||||||
|
); |
||||||
|
|
||||||
|
match &nf.unit_options { |
||||||
|
UnitFormatOptions::Currency { |
||||||
|
currency, |
||||||
|
display, |
||||||
|
sign, |
||||||
|
} => { |
||||||
|
options.property( |
||||||
|
js_string!("currency"), |
||||||
|
currency.to_js_string(), |
||||||
|
Attribute::all(), |
||||||
|
); |
||||||
|
options.property( |
||||||
|
js_string!("currencyDisplay"), |
||||||
|
display.to_js_string(), |
||||||
|
Attribute::all(), |
||||||
|
); |
||||||
|
options.property( |
||||||
|
js_string!("currencySign"), |
||||||
|
sign.to_js_string(), |
||||||
|
Attribute::all(), |
||||||
|
); |
||||||
|
} |
||||||
|
UnitFormatOptions::Unit { unit, display } => { |
||||||
|
options.property(js_string!("unit"), unit.to_js_string(), Attribute::all()); |
||||||
|
options.property( |
||||||
|
js_string!("unitDisplay"), |
||||||
|
display.to_js_string(), |
||||||
|
Attribute::all(), |
||||||
|
); |
||||||
|
} |
||||||
|
UnitFormatOptions::Decimal | UnitFormatOptions::Percent => {} |
||||||
|
} |
||||||
|
|
||||||
|
options.property( |
||||||
|
js_string!("minimumIntegerDigits"), |
||||||
|
nf.digit_options.minimum_integer_digits, |
||||||
|
Attribute::all(), |
||||||
|
); |
||||||
|
|
||||||
|
if let Some(Extrema { minimum, maximum }) = nf.digit_options.rounding_type.fraction_digits() |
||||||
|
{ |
||||||
|
options |
||||||
|
.property( |
||||||
|
js_string!("minimumFractionDigits"), |
||||||
|
minimum, |
||||||
|
Attribute::all(), |
||||||
|
) |
||||||
|
.property( |
||||||
|
js_string!("maximumFractionDigits"), |
||||||
|
maximum, |
||||||
|
Attribute::all(), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
if let Some(Extrema { minimum, maximum }) = |
||||||
|
nf.digit_options.rounding_type.significant_digits() |
||||||
|
{ |
||||||
|
options |
||||||
|
.property( |
||||||
|
js_string!("minimumSignificantDigits"), |
||||||
|
minimum, |
||||||
|
Attribute::all(), |
||||||
|
) |
||||||
|
.property( |
||||||
|
js_string!("maximumSignificantDigits"), |
||||||
|
maximum, |
||||||
|
Attribute::all(), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
let use_grouping = match nf.use_grouping { |
||||||
|
GroupingStrategy::Auto => js_string!("auto").into(), |
||||||
|
GroupingStrategy::Never => JsValue::from(false), |
||||||
|
GroupingStrategy::Always => js_string!("always").into(), |
||||||
|
GroupingStrategy::Min2 => js_string!("min2").into(), |
||||||
|
_ => { |
||||||
|
return Err(JsNativeError::typ() |
||||||
|
.with_message("unsupported useGrouping value") |
||||||
|
.into()) |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
options |
||||||
|
.property(js_string!("useGrouping"), use_grouping, Attribute::all()) |
||||||
|
.property( |
||||||
|
js_string!("notation"), |
||||||
|
nf.notation.kind().to_js_string(), |
||||||
|
Attribute::all(), |
||||||
|
); |
||||||
|
|
||||||
|
if let Notation::Compact { display } = nf.notation { |
||||||
|
options.property( |
||||||
|
js_string!("compactDisplay"), |
||||||
|
display.to_js_string(), |
||||||
|
Attribute::all(), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
let sign_display = match nf.sign_display { |
||||||
|
SignDisplay::Auto => js_string!("auto"), |
||||||
|
SignDisplay::Never => js_string!("never"), |
||||||
|
SignDisplay::Always => js_string!("always"), |
||||||
|
SignDisplay::ExceptZero => js_string!("exceptZero"), |
||||||
|
SignDisplay::Negative => js_string!("negative"), |
||||||
|
_ => { |
||||||
|
return Err(JsNativeError::typ() |
||||||
|
.with_message("unsupported signDisplay value") |
||||||
|
.into()) |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
options |
||||||
|
.property(js_string!("signDisplay"), sign_display, Attribute::all()) |
||||||
|
.property( |
||||||
|
js_string!("roundingIncrement"), |
||||||
|
nf.digit_options.rounding_increment.to_u16(), |
||||||
|
Attribute::all(), |
||||||
|
) |
||||||
|
.property( |
||||||
|
js_string!("roundingPriority"), |
||||||
|
nf.digit_options.rounding_priority.to_js_string(), |
||||||
|
Attribute::all(), |
||||||
|
) |
||||||
|
.property( |
||||||
|
js_string!("trailingZeroDisplay"), |
||||||
|
nf.digit_options.trailing_zero_display.to_js_string(), |
||||||
|
Attribute::all(), |
||||||
|
); |
||||||
|
|
||||||
|
// 6. Return options.
|
||||||
|
Ok(options.build().into()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Abstract operation [`UnwrapNumberFormat ( nf )`][spec].
|
||||||
|
///
|
||||||
|
/// This also checks that the returned object is a `NumberFormat`, which skips the
|
||||||
|
/// call to `RequireInternalSlot`.
|
||||||
|
///
|
||||||
|
/// [spec]: https://tc39.es/ecma402/#sec-unwrapnumberformat
|
||||||
|
fn unwrap_number_format(nf: &JsValue, context: &mut Context) -> JsResult<JsObject<NumberFormat>> { |
||||||
|
// 1. If Type(nf) is not Object, throw a TypeError exception.
|
||||||
|
let nf_o = nf.as_object().ok_or_else(|| { |
||||||
|
JsNativeError::typ().with_message("value was not an `Intl.NumberFormat` object") |
||||||
|
})?; |
||||||
|
|
||||||
|
if let Ok(nf) = nf_o.clone().downcast::<NumberFormat>() { |
||||||
|
// 3. Return nf.
|
||||||
|
return Ok(nf); |
||||||
|
} |
||||||
|
|
||||||
|
// 2. If nf does not have an [[InitializedNumberFormat]] internal slot and ? OrdinaryHasInstance(%Intl.NumberFormat%, nf)
|
||||||
|
// is true, then
|
||||||
|
let constructor = context |
||||||
|
.intrinsics() |
||||||
|
.constructors() |
||||||
|
.number_format() |
||||||
|
.constructor(); |
||||||
|
if JsValue::ordinary_has_instance(&constructor.into(), nf, context)? { |
||||||
|
let fallback_symbol = context |
||||||
|
.intrinsics() |
||||||
|
.objects() |
||||||
|
.intl() |
||||||
|
.borrow() |
||||||
|
.data |
||||||
|
.fallback_symbol(); |
||||||
|
|
||||||
|
// a. Return ? Get(nf, %Intl%.[[FallbackSymbol]]).
|
||||||
|
let nf = nf_o.get(fallback_symbol, context)?; |
||||||
|
if let JsValue::Object(nf) = nf { |
||||||
|
if let Ok(nf) = nf.downcast::<NumberFormat>() { |
||||||
|
return Ok(nf); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Err(JsNativeError::typ() |
||||||
|
.with_message("object was not an `Intl.NumberFormat` object") |
||||||
|
.into()) |
||||||
|
} |
||||||
|
|
||||||
|
/// Abstract operation [`ToIntlMathematicalValue ( value )`][spec].
|
||||||
|
///
|
||||||
|
/// [spec]: https://tc39.es/ecma402/#sec-tointlmathematicalvalue
|
||||||
|
fn to_intl_mathematical_value(value: &JsValue, context: &mut Context) -> JsResult<FixedDecimal> { |
||||||
|
// 1. Let primValue be ? ToPrimitive(value, number).
|
||||||
|
let prim_value = value.to_primitive(context, PreferredType::Number)?; |
||||||
|
|
||||||
|
// TODO: Add support in `FixedDecimal` for infinity and NaN, which
|
||||||
|
// should remove the returned errors.
|
||||||
|
match prim_value { |
||||||
|
// 2. If Type(primValue) is BigInt, return ℝ(primValue).
|
||||||
|
JsValue::BigInt(bi) => { |
||||||
|
let bi = bi.to_string(); |
||||||
|
FixedDecimal::try_from(bi.as_bytes()) |
||||||
|
.map_err(|err| JsNativeError::range().with_message(err.to_string()).into()) |
||||||
|
} |
||||||
|
// 3. If Type(primValue) is String, then
|
||||||
|
// a. Let str be primValue.
|
||||||
|
JsValue::String(s) => { |
||||||
|
// 5. Let text be StringToCodePoints(str).
|
||||||
|
// 6. Let literal be ParseText(text, StringNumericLiteral).
|
||||||
|
// 7. If literal is a List of errors, return not-a-number.
|
||||||
|
// 8. Let intlMV be the StringIntlMV of literal.
|
||||||
|
// 9. If intlMV is a mathematical value, then
|
||||||
|
// a. Let rounded be RoundMVResult(abs(intlMV)).
|
||||||
|
// b. If rounded is +∞𝔽 and intlMV < 0, return negative-infinity.
|
||||||
|
// c. If rounded is +∞𝔽, return positive-infinity.
|
||||||
|
// d. If rounded is +0𝔽 and intlMV < 0, return negative-zero.
|
||||||
|
// e. If rounded is +0𝔽, return 0.
|
||||||
|
js_string_to_fixed_decimal(&s).ok_or_else(|| { |
||||||
|
JsNativeError::syntax() |
||||||
|
.with_message("could not parse the provided string") |
||||||
|
.into() |
||||||
|
}) |
||||||
|
} |
||||||
|
// 4. Else,
|
||||||
|
other => { |
||||||
|
// a. Let x be ? ToNumber(primValue).
|
||||||
|
// b. If x is -0𝔽, return negative-zero.
|
||||||
|
// c. Let str be Number::toString(x, 10).
|
||||||
|
let x = other.to_number(context)?; |
||||||
|
|
||||||
|
FixedDecimal::try_from_f64(x, FloatPrecision::Floating) |
||||||
|
.map_err(|err| JsNativeError::range().with_message(err.to_string()).into()) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Abstract operation [`StringToNumber ( str )`][spec], but specialized for the conversion
|
||||||
|
/// to a `FixedDecimal`.
|
||||||
|
///
|
||||||
|
/// [spec]: https://tc39.es/ecma262/#sec-stringtonumber
|
||||||
|
// TODO: Introduce `Infinity` and `NaN` to `FixedDecimal` to make this operation
|
||||||
|
// infallible.
|
||||||
|
pub(crate) fn js_string_to_fixed_decimal(string: &JsString) -> Option<FixedDecimal> { |
||||||
|
// 1. Let text be ! StringToCodePoints(str).
|
||||||
|
// 2. Let literal be ParseText(text, StringNumericLiteral).
|
||||||
|
let Ok(string) = string.to_std_string() else { |
||||||
|
// 3. If literal is a List of errors, return NaN.
|
||||||
|
return None; |
||||||
|
}; |
||||||
|
// 4. Return StringNumericValue of literal.
|
||||||
|
let string = string.trim_matches(is_trimmable_whitespace); |
||||||
|
match string { |
||||||
|
"" => return Some(FixedDecimal::from(0)), |
||||||
|
"-Infinity" | "Infinity" | "+Infinity" => return None, |
||||||
|
_ => {} |
||||||
|
} |
||||||
|
|
||||||
|
let mut s = string.bytes(); |
||||||
|
let base = match (s.next(), s.next()) { |
||||||
|
(Some(b'0'), Some(b'b' | b'B')) => Some(2), |
||||||
|
(Some(b'0'), Some(b'o' | b'O')) => Some(8), |
||||||
|
(Some(b'0'), Some(b'x' | b'X')) => Some(16), |
||||||
|
// Make sure that no further variants of "infinity" are parsed.
|
||||||
|
(Some(b'i' | b'I'), _) => { |
||||||
|
return None; |
||||||
|
} |
||||||
|
_ => None, |
||||||
|
}; |
||||||
|
|
||||||
|
// Parse numbers that begin with `0b`, `0o` and `0x`.
|
||||||
|
let s = if let Some(base) = base { |
||||||
|
let string = &string[2..]; |
||||||
|
if string.is_empty() { |
||||||
|
return None; |
||||||
|
} |
||||||
|
let int = BigInt::from_str_radix(string, base).ok()?; |
||||||
|
let int_str = int.to_string(); |
||||||
|
|
||||||
|
Cow::Owned(int_str) |
||||||
|
} else { |
||||||
|
Cow::Borrowed(string) |
||||||
|
}; |
||||||
|
|
||||||
|
FixedDecimal::try_from(s.as_bytes()).ok() |
||||||
|
} |
||||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,41 @@ |
|||||||
|
use crate::builtins::intl::number_format::RoundingIncrement; |
||||||
|
use fixed_decimal::RoundingIncrement::*; |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn u16_to_rounding_increment_sunny_day() { |
||||||
|
#[rustfmt::skip] |
||||||
|
let valid_cases: [(u16, RoundingIncrement); 15] = [ |
||||||
|
// Singles
|
||||||
|
(1, RoundingIncrement::from_parts(MultiplesOf1, 0).unwrap()), |
||||||
|
(2, RoundingIncrement::from_parts(MultiplesOf2, 0).unwrap()), |
||||||
|
(5, RoundingIncrement::from_parts(MultiplesOf5, 0).unwrap()), |
||||||
|
// Tens
|
||||||
|
(10, RoundingIncrement::from_parts(MultiplesOf1, 1).unwrap()), |
||||||
|
(20, RoundingIncrement::from_parts(MultiplesOf2, 1).unwrap()), |
||||||
|
(25, RoundingIncrement::from_parts(MultiplesOf25, 0).unwrap()), |
||||||
|
(50, RoundingIncrement::from_parts(MultiplesOf5, 1).unwrap()), |
||||||
|
// Hundreds
|
||||||
|
(100, RoundingIncrement::from_parts(MultiplesOf1, 2).unwrap()), |
||||||
|
(200, RoundingIncrement::from_parts(MultiplesOf2, 2).unwrap()), |
||||||
|
(250, RoundingIncrement::from_parts(MultiplesOf25, 1).unwrap()), |
||||||
|
(500, RoundingIncrement::from_parts(MultiplesOf5, 2).unwrap()), |
||||||
|
// Thousands
|
||||||
|
(1000, RoundingIncrement::from_parts(MultiplesOf1, 3).unwrap()), |
||||||
|
(2000, RoundingIncrement::from_parts(MultiplesOf2, 3).unwrap()), |
||||||
|
(2500, RoundingIncrement::from_parts(MultiplesOf25, 2).unwrap()), |
||||||
|
(5000, RoundingIncrement::from_parts(MultiplesOf5, 3).unwrap()), |
||||||
|
]; |
||||||
|
|
||||||
|
for (num, increment) in valid_cases { |
||||||
|
assert_eq!(RoundingIncrement::from_u16(num), Some(increment)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
#[test] |
||||||
|
fn u16_to_rounding_increment_rainy_day() { |
||||||
|
const INVALID_CASES: [u16; 9] = [0, 4, 6, 24, 10000, 65535, 7373, 140, 1500]; |
||||||
|
|
||||||
|
for num in INVALID_CASES { |
||||||
|
assert!(RoundingIncrement::from_u16(num).is_none()); |
||||||
|
} |
||||||
|
} |
@ -1,525 +0,0 @@ |
|||||||
use boa_macros::utf16; |
|
||||||
use fixed_decimal::{FixedDecimal, FloatPrecision, RoundingIncrement as BaseMultiple}; |
|
||||||
|
|
||||||
use crate::{ |
|
||||||
builtins::{ |
|
||||||
intl::{ |
|
||||||
number_format::{Extrema, RoundingType, TrailingZeroDisplay}, |
|
||||||
options::{default_number_option, get_number_option}, |
|
||||||
}, |
|
||||||
options::{get_option, RoundingMode}, |
|
||||||
}, |
|
||||||
Context, JsNativeError, JsObject, JsResult, |
|
||||||
}; |
|
||||||
|
|
||||||
use super::{DigitFormatOptions, Notation, RoundingPriority}; |
|
||||||
|
|
||||||
/// The increment of a rounding operation.
|
|
||||||
///
|
|
||||||
/// This differs from [`fixed_decimal::RoundingIncrement`] because ECMA402 accepts
|
|
||||||
/// several more increments than `fixed_decimal`, but all increments can be decomposed
|
|
||||||
/// into the target multiple and the magnitude offset.
|
|
||||||
///
|
|
||||||
/// For example, rounding the number `0.02456` to the increment 200 at position
|
|
||||||
/// -3 is equivalent to rounding the same number to the increment 2 at position -1, and adding
|
|
||||||
/// trailing zeroes.
|
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)] |
|
||||||
pub(crate) struct RoundingIncrement { |
|
||||||
multiple: BaseMultiple, |
|
||||||
// INVARIANT: can only be 0, 1, 2, or 3
|
|
||||||
magnitude_offset: u8, |
|
||||||
} |
|
||||||
|
|
||||||
impl RoundingIncrement { |
|
||||||
/// Creates a `RoundingIncrement` from the numeric value of the increment.
|
|
||||||
fn from_u16(increment: u16) -> Option<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].
|
|
||||||
///
|
|
||||||
/// 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> { |
|
||||||
// 1. Let mnid be ? GetNumberOption(options, "minimumIntegerDigits,", 1, 21, 1).
|
|
||||||
let minimum_integer_digits = |
|
||||||
get_number_option(options, utf16!("minimumIntegerDigits"), 1, 21, context)?.unwrap_or(1); |
|
||||||
// 2. Let mnfd be ? Get(options, "minimumFractionDigits").
|
|
||||||
let min_float_digits = options.get(utf16!("minimumFractionDigits"), context)?; |
|
||||||
// 3. Let mxfd be ? Get(options, "maximumFractionDigits").
|
|
||||||
let max_float_digits = options.get(utf16!("maximumFractionDigits"), context)?; |
|
||||||
// 4. Let mnsd be ? Get(options, "minimumSignificantDigits").
|
|
||||||
let min_sig_digits = options.get(utf16!("minimumSignificantDigits"), context)?; |
|
||||||
// 5. Let mxsd be ? Get(options, "maximumSignificantDigits").
|
|
||||||
let max_sig_digits = options.get(utf16!("maximumSignificantDigits"), context)?; |
|
||||||
|
|
||||||
// 7. Let roundingPriority be ? GetOption(options, "roundingPriority", string, « "auto", "morePrecision", "lessPrecision" », "auto").
|
|
||||||
let mut rounding_priority = |
|
||||||
get_option(options, utf16!("roundingPriority"), context)?.unwrap_or_default(); |
|
||||||
|
|
||||||
// 8. Let roundingIncrement be ? GetNumberOption(options, "roundingIncrement", 1, 5000, 1).
|
|
||||||
// 9. If roundingIncrement is not in « 1, 2, 5, 10, 20, 25, 50, 100, 200, 250, 500, 1000, 2000, 2500, 5000 », throw a RangeError exception.
|
|
||||||
let rounding_increment = |
|
||||||
get_number_option(options, utf16!("roundingIncrement"), 1, 5000, context)?.unwrap_or(1); |
|
||||||
|
|
||||||
let rounding_increment = RoundingIncrement::from_u16(rounding_increment).ok_or_else(|| { |
|
||||||
JsNativeError::range().with_message("invalid value for option `roundingIncrement`") |
|
||||||
})?; |
|
||||||
|
|
||||||
// 10. Let roundingMode be ? GetOption(options, "roundingMode", string, « "ceil", "floor", "expand", "trunc", "halfCeil", "halfFloor", "halfExpand", "halfTrunc", "halfEven" », "halfExpand").
|
|
||||||
let rounding_mode = get_option(options, utf16!("roundingMode"), context)?.unwrap_or_default(); |
|
||||||
|
|
||||||
// 11. Let trailingZeroDisplay be ? GetOption(options, "trailingZeroDisplay", string, « "auto", "stripIfInteger" », "auto").
|
|
||||||
let trailing_zero_display = |
|
||||||
get_option(options, utf16!("trailingZeroDisplay"), context)?.unwrap_or_default(); |
|
||||||
|
|
||||||
// 12. NOTE: All fields required by SetNumberFormatDigitOptions have now been read from options. The remainder of this AO interprets the options and may throw exceptions.
|
|
||||||
|
|
||||||
// 13. If roundingIncrement is not 1, set mxfdDefault to mnfdDefault.
|
|
||||||
if rounding_increment.to_u16() != 1 { |
|
||||||
max_float_digits_default = min_float_digits_default; |
|
||||||
} |
|
||||||
|
|
||||||
// 17. If mnsd is not undefined or mxsd is not undefined, then
|
|
||||||
// a. Let hasSd be true.
|
|
||||||
// 18. Else,
|
|
||||||
// a. Let hasSd be false.
|
|
||||||
let has_sig_limits = !min_sig_digits.is_undefined() || !max_sig_digits.is_undefined(); |
|
||||||
|
|
||||||
// 19. If mnfd is not undefined or mxfd is not undefined, then
|
|
||||||
// a. Let hasFd be true.
|
|
||||||
// 20. Else,
|
|
||||||
// a. Let hasFd be false.
|
|
||||||
let has_float_limits = !min_float_digits.is_undefined() || !max_float_digits.is_undefined(); |
|
||||||
|
|
||||||
// 21. Let needSd be true.
|
|
||||||
// 22. Let needFd be true.
|
|
||||||
let (need_sig_limits, need_frac_limits) = if rounding_priority == RoundingPriority::Auto { |
|
||||||
// 23. If roundingPriority is "auto", then
|
|
||||||
// a. Set needSd to hasSd.
|
|
||||||
// b. If needSd is true, or hasFd is false and notation is "compact", then
|
|
||||||
// i. Set needFd to false.
|
|
||||||
( |
|
||||||
has_sig_limits, |
|
||||||
!has_sig_limits && (has_float_limits || notation != Notation::Compact), |
|
||||||
) |
|
||||||
} else { |
|
||||||
(true, true) |
|
||||||
}; |
|
||||||
|
|
||||||
// 24. If needSd is true, then
|
|
||||||
let sig_digits = if need_sig_limits { |
|
||||||
// a. If hasSd is true, then
|
|
||||||
let extrema = if has_sig_limits { |
|
||||||
// i. Set intlObj.[[MinimumSignificantDigits]] to ? DefaultNumberOption(mnsd, 1, 21, 1).
|
|
||||||
let min_sig = default_number_option(&min_sig_digits, 1, 21, context)?.unwrap_or(1); |
|
||||||
// ii. Set intlObj.[[MaximumSignificantDigits]] to ? DefaultNumberOption(mxsd, intlObj.[[MinimumSignificantDigits]], 21, 21).
|
|
||||||
let max_sig = |
|
||||||
default_number_option(&max_sig_digits, min_sig, 21, context)?.unwrap_or(21); |
|
||||||
|
|
||||||
Extrema { |
|
||||||
minimum: min_sig, |
|
||||||
maximum: max_sig, |
|
||||||
} |
|
||||||
} else { |
|
||||||
// b. Else,
|
|
||||||
Extrema { |
|
||||||
// i. Set intlObj.[[MinimumSignificantDigits]] to 1.
|
|
||||||
minimum: 1, |
|
||||||
// ii. Set intlObj.[[MaximumSignificantDigits]] to 21.
|
|
||||||
maximum: 21, |
|
||||||
} |
|
||||||
}; |
|
||||||
assert!(extrema.minimum <= extrema.maximum); |
|
||||||
Some(extrema) |
|
||||||
} else { |
|
||||||
None |
|
||||||
}; |
|
||||||
|
|
||||||
// 25. If needFd is true, then
|
|
||||||
let fractional_digits = if need_frac_limits { |
|
||||||
// a. If hasFd is true, then
|
|
||||||
let extrema = if has_float_limits { |
|
||||||
// i. Set mnfd to ? DefaultNumberOption(mnfd, 0, 100, undefined).
|
|
||||||
let min_float_digits = default_number_option(&min_float_digits, 0, 100, context)?; |
|
||||||
// ii. Set mxfd to ? DefaultNumberOption(mxfd, 0, 100, undefined).
|
|
||||||
let max_float_digits = default_number_option(&max_float_digits, 0, 100, context)?; |
|
||||||
|
|
||||||
let (min_float_digits, max_float_digits) = match (min_float_digits, max_float_digits) { |
|
||||||
(Some(min_float_digits), Some(max_float_digits)) => { |
|
||||||
// v. Else if mnfd is greater than mxfd, throw a RangeError exception.
|
|
||||||
if min_float_digits > max_float_digits { |
|
||||||
return Err(JsNativeError::range().with_message( |
|
||||||
"`minimumFractionDigits` cannot be bigger than `maximumFractionDigits`", |
|
||||||
).into()); |
|
||||||
} |
|
||||||
(min_float_digits, max_float_digits) |
|
||||||
} |
|
||||||
// iv. Else if mxfd is undefined, set mxfd to max(mxfdDefault, mnfd).
|
|
||||||
(Some(min_float_digits), None) => ( |
|
||||||
min_float_digits, |
|
||||||
u8::max(max_float_digits_default, min_float_digits), |
|
||||||
), |
|
||||||
// iii. If mnfd is undefined, set mnfd to min(mnfdDefault, mxfd).
|
|
||||||
(None, Some(max_float_digits)) => ( |
|
||||||
u8::min(min_float_digits_default, max_float_digits), |
|
||||||
max_float_digits, |
|
||||||
), |
|
||||||
(None, None) => { |
|
||||||
unreachable!("`has_fd` can only be true if `mnfd` or `mxfd` is not undefined") |
|
||||||
} |
|
||||||
}; |
|
||||||
|
|
||||||
Extrema { |
|
||||||
// vi. Set intlObj.[[MinimumFractionDigits]] to mnfd.
|
|
||||||
minimum: min_float_digits, |
|
||||||
// vii. Set intlObj.[[MaximumFractionDigits]] to mxfd.
|
|
||||||
maximum: max_float_digits, |
|
||||||
} |
|
||||||
} else { |
|
||||||
// b. Else,
|
|
||||||
Extrema { |
|
||||||
// i. Set intlObj.[[MinimumFractionDigits]] to mnfdDefault.
|
|
||||||
minimum: min_float_digits_default, |
|
||||||
// ii. Set intlObj.[[MaximumFractionDigits]] to mxfdDefault.
|
|
||||||
maximum: max_float_digits_default, |
|
||||||
} |
|
||||||
}; |
|
||||||
assert!(extrema.minimum <= extrema.maximum); |
|
||||||
Some(extrema) |
|
||||||
} else { |
|
||||||
None |
|
||||||
}; |
|
||||||
|
|
||||||
let rounding_type = match (sig_digits, fractional_digits) { |
|
||||||
// 26. If needSd is false and needFd is false, then
|
|
||||||
(None, None) => { |
|
||||||
// f. Set intlObj.[[ComputedRoundingPriority]] to "morePrecision".
|
|
||||||
rounding_priority = RoundingPriority::MorePrecision; |
|
||||||
// e. Set intlObj.[[RoundingType]] to morePrecision.
|
|
||||||
RoundingType::MorePrecision { |
|
||||||
significant_digits: Extrema { |
|
||||||
// c. Set intlObj.[[MinimumSignificantDigits]] to 1.
|
|
||||||
minimum: 1, |
|
||||||
// d. Set intlObj.[[MaximumSignificantDigits]] to 2.
|
|
||||||
maximum: 2, |
|
||||||
}, |
|
||||||
fraction_digits: Extrema { |
|
||||||
// a. Set intlObj.[[MinimumFractionDigits]] to 0.
|
|
||||||
minimum: 0, |
|
||||||
// b. Set intlObj.[[MaximumFractionDigits]] to 0.
|
|
||||||
maximum: 0, |
|
||||||
}, |
|
||||||
} |
|
||||||
} |
|
||||||
(Some(significant_digits), Some(fraction_digits)) => match rounding_priority { |
|
||||||
RoundingPriority::MorePrecision => RoundingType::MorePrecision { |
|
||||||
significant_digits, |
|
||||||
fraction_digits, |
|
||||||
}, |
|
||||||
RoundingPriority::LessPrecision => RoundingType::LessPrecision { |
|
||||||
significant_digits, |
|
||||||
fraction_digits, |
|
||||||
}, |
|
||||||
RoundingPriority::Auto => { |
|
||||||
unreachable!("Cannot have both roundings when the priority is `Auto`") |
|
||||||
} |
|
||||||
}, |
|
||||||
(Some(sig), None) => RoundingType::SignificantDigits(sig), |
|
||||||
(None, Some(frac)) => RoundingType::FractionDigits(frac), |
|
||||||
}; |
|
||||||
|
|
||||||
if rounding_increment.to_u16() != 1 { |
|
||||||
let RoundingType::FractionDigits(range) = rounding_type else { |
|
||||||
return Err(JsNativeError::typ() |
|
||||||
.with_message("option `roundingIncrement` invalid for the current set of options") |
|
||||||
.into()); |
|
||||||
}; |
|
||||||
|
|
||||||
if range.minimum != range.maximum { |
|
||||||
return Err(JsNativeError::range() |
|
||||||
.with_message("option `roundingIncrement` invalid for the current set of options") |
|
||||||
.into()); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
Ok(DigitFormatOptions { |
|
||||||
// 6. Set intlObj.[[MinimumIntegerDigits]] to mnid.
|
|
||||||
minimum_integer_digits, |
|
||||||
// 14. Set intlObj.[[RoundingIncrement]] to roundingIncrement.
|
|
||||||
rounding_increment, |
|
||||||
// 15. Set intlObj.[[RoundingMode]] to roundingMode.
|
|
||||||
rounding_mode, |
|
||||||
// 16. Set intlObj.[[TrailingZeroDisplay]] to trailingZeroDisplay.
|
|
||||||
trailing_zero_display, |
|
||||||
rounding_type, |
|
||||||
rounding_priority, |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
/// Abstract operation [`FormatNumericToString ( intlObject, x )`][spec].
|
|
||||||
///
|
|
||||||
/// Converts the input number to a `FixedDecimal` with the specified digit format options.
|
|
||||||
///
|
|
||||||
/// [spec]: https://tc39.es/ecma402/#sec-formatnumberstring
|
|
||||||
pub(crate) fn f64_to_formatted_fixed_decimal( |
|
||||||
number: f64, |
|
||||||
options: &DigitFormatOptions, |
|
||||||
) -> FixedDecimal { |
|
||||||
fn round(number: &mut FixedDecimal, position: i16, mode: RoundingMode, multiple: BaseMultiple) { |
|
||||||
match mode { |
|
||||||
RoundingMode::Ceil => number.ceil_to_increment(position, multiple), |
|
||||||
RoundingMode::Floor => number.floor_to_increment(position, multiple), |
|
||||||
RoundingMode::Expand => number.expand_to_increment(position, multiple), |
|
||||||
RoundingMode::Trunc => number.trunc_to_increment(position, multiple), |
|
||||||
RoundingMode::HalfCeil => number.half_ceil_to_increment(position, multiple), |
|
||||||
RoundingMode::HalfFloor => number.half_floor_to_increment(position, multiple), |
|
||||||
RoundingMode::HalfExpand => number.half_expand_to_increment(position, multiple), |
|
||||||
RoundingMode::HalfTrunc => number.half_trunc_to_increment(position, multiple), |
|
||||||
RoundingMode::HalfEven => number.half_even_to_increment(position, multiple), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// <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, BaseMultiple::MultiplesOf1); |
|
||||||
max_msb |
|
||||||
} |
|
||||||
|
|
||||||
// <https://tc39.es/ecma402/#sec-torawfixed>
|
|
||||||
fn to_raw_fixed( |
|
||||||
number: &mut FixedDecimal, |
|
||||||
min_fraction: u8, |
|
||||||
max_fraction: u8, |
|
||||||
rounding_increment: RoundingIncrement, |
|
||||||
rounding_mode: RoundingMode, |
|
||||||
) -> i16 { |
|
||||||
#[cfg(debug_assertions)] |
|
||||||
if rounding_increment.to_u16() != 1 { |
|
||||||
assert_eq!(min_fraction, max_fraction); |
|
||||||
} |
|
||||||
|
|
||||||
number.pad_end(-i16::from(min_fraction)); |
|
||||||
round( |
|
||||||
number, |
|
||||||
rounding_increment.magnitude_offset() - i16::from(max_fraction), |
|
||||||
rounding_mode, |
|
||||||
rounding_increment.multiple, |
|
||||||
); |
|
||||||
-i16::from(max_fraction) |
|
||||||
} |
|
||||||
|
|
||||||
// 1. If x is negative-zero, then
|
|
||||||
// a. Let isNegative be true.
|
|
||||||
// b. Set x to 0.
|
|
||||||
// 2. Else,
|
|
||||||
// a. Assert: x is a mathematical value.
|
|
||||||
// b. If x < 0, let isNegative be true; else let isNegative be false.
|
|
||||||
// c. If isNegative is true, then
|
|
||||||
// i. Set x to -x.
|
|
||||||
// We can skip these steps, because `FixedDecimal` already provides support for
|
|
||||||
// negative zeroes.
|
|
||||||
let mut number = FixedDecimal::try_from_f64(number, FloatPrecision::Floating) |
|
||||||
.expect("`number` must be finite"); |
|
||||||
|
|
||||||
// 3. Let unsignedRoundingMode be GetUnsignedRoundingMode(intlObject.[[RoundingMode]], isNegative).
|
|
||||||
// Skipping because `FixedDecimal`'s API already provides methods equivalent to `RoundingMode`s.
|
|
||||||
|
|
||||||
match options.rounding_type { |
|
||||||
// 4. If intlObject.[[RoundingType]] is significantDigits, then
|
|
||||||
RoundingType::SignificantDigits(Extrema { minimum, maximum }) => { |
|
||||||
// a. Let result be ToRawPrecision(x, intlObject.[[MinimumSignificantDigits]], intlObject.[[MaximumSignificantDigits]], unsignedRoundingMode).
|
|
||||||
to_raw_precision(&mut number, minimum, maximum, options.rounding_mode); |
|
||||||
} |
|
||||||
// 5. Else if intlObject.[[RoundingType]] is fractionDigits, then
|
|
||||||
RoundingType::FractionDigits(Extrema { minimum, maximum }) => { |
|
||||||
// a. Let result be ToRawFixed(x, intlObject.[[MinimumFractionDigits]], intlObject.[[MaximumFractionDigits]], intlObject.[[RoundingIncrement]], unsignedRoundingMode).
|
|
||||||
to_raw_fixed( |
|
||||||
&mut number, |
|
||||||
minimum, |
|
||||||
maximum, |
|
||||||
options.rounding_increment, |
|
||||||
options.rounding_mode, |
|
||||||
); |
|
||||||
} |
|
||||||
// 6. Else,
|
|
||||||
RoundingType::MorePrecision { |
|
||||||
significant_digits, |
|
||||||
fraction_digits, |
|
||||||
} |
|
||||||
| RoundingType::LessPrecision { |
|
||||||
significant_digits, |
|
||||||
fraction_digits, |
|
||||||
} => { |
|
||||||
let prefer_more_precision = |
|
||||||
matches!(options.rounding_type, RoundingType::MorePrecision { .. }); |
|
||||||
// a. Let sResult be ToRawPrecision(x, intlObject.[[MinimumSignificantDigits]], intlObject.[[MaximumSignificantDigits]], unsignedRoundingMode).
|
|
||||||
let mut fixed = number.clone(); |
|
||||||
let s_magnitude = to_raw_precision( |
|
||||||
&mut number, |
|
||||||
significant_digits.maximum, |
|
||||||
significant_digits.minimum, |
|
||||||
options.rounding_mode, |
|
||||||
); |
|
||||||
// b. Let fResult be ToRawFixed(x, intlObject.[[MinimumFractionDigits]], intlObject.[[MaximumFractionDigits]], intlObject.[[RoundingIncrement]], unsignedRoundingMode).
|
|
||||||
let f_magnitude = to_raw_fixed( |
|
||||||
&mut fixed, |
|
||||||
fraction_digits.maximum, |
|
||||||
fraction_digits.minimum, |
|
||||||
options.rounding_increment, |
|
||||||
options.rounding_mode, |
|
||||||
); |
|
||||||
|
|
||||||
// c. If intlObject.[[RoundingType]] is morePrecision, then
|
|
||||||
// i. If sResult.[[RoundingMagnitude]] ≤ fResult.[[RoundingMagnitude]], then
|
|
||||||
// 1. Let result be sResult.
|
|
||||||
// ii. Else,
|
|
||||||
// 1. Let result be fResult.
|
|
||||||
// d. Else,
|
|
||||||
// i. Assert: intlObject.[[RoundingType]] is lessPrecision.
|
|
||||||
// ii. If sResult.[[RoundingMagnitude]] ≤ fResult.[[RoundingMagnitude]], then
|
|
||||||
// 1. Let result be fResult.
|
|
||||||
// iii. Else,
|
|
||||||
// 1. Let result be sResult.
|
|
||||||
if (prefer_more_precision && f_magnitude < s_magnitude) |
|
||||||
|| (!prefer_more_precision && s_magnitude <= f_magnitude) |
|
||||||
{ |
|
||||||
number = fixed; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// 7. Set x to result.[[RoundedNumber]].
|
|
||||||
// 8. Let string be result.[[FormattedString]].
|
|
||||||
// 9. If intlObject.[[TrailingZeroDisplay]] is "stripIfInteger" and x modulo 1 = 0, then
|
|
||||||
if options.trailing_zero_display == TrailingZeroDisplay::StripIfInteger |
|
||||||
&& number.nonzero_magnitude_end() >= 0 |
|
||||||
{ |
|
||||||
// a. Let i be StringIndexOf(string, ".", 0).
|
|
||||||
// b. If i ≠ -1, set string to the substring of string from 0 to i.
|
|
||||||
number.trim_end(); |
|
||||||
} |
|
||||||
|
|
||||||
// 10. Let int be result.[[IntegerDigitsCount]].
|
|
||||||
// 11. Let minInteger be intlObject.[[MinimumIntegerDigits]].
|
|
||||||
// 12. If int < minInteger, then
|
|
||||||
// a. Let forwardZeros be the String consisting of minInteger - int occurrences of the code unit 0x0030 (DIGIT ZERO).
|
|
||||||
// b. Set string to the string-concatenation of forwardZeros and string.
|
|
||||||
number.pad_start(i16::from(options.minimum_integer_digits)); |
|
||||||
|
|
||||||
// 13. If isNegative is true, then
|
|
||||||
// a. If x is 0, set x to negative-zero. Otherwise, set x to -x.
|
|
||||||
// As mentioned above, `FixedDecimal` has support for this.
|
|
||||||
|
|
||||||
// 14. Return the Record { [[RoundedNumber]]: x, [[FormattedString]]: string }.
|
|
||||||
number |
|
||||||
} |
|
||||||
|
|
||||||
#[cfg(test)] |
|
||||||
mod tests { |
|
||||||
use crate::builtins::intl::number_format::RoundingIncrement; |
|
||||||
use fixed_decimal::RoundingIncrement::*; |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn u16_to_rounding_increment_sunny_day() { |
|
||||||
#[rustfmt::skip] |
|
||||||
const VALID_CASES: [(u16, RoundingIncrement); 15] = [ |
|
||||||
// Singles
|
|
||||||
(1, RoundingIncrement { multiple: MultiplesOf1, magnitude_offset: 0 }), |
|
||||||
(2, RoundingIncrement { multiple: MultiplesOf2, magnitude_offset: 0 }), |
|
||||||
(5, RoundingIncrement { multiple: MultiplesOf5, magnitude_offset: 0 }), |
|
||||||
// Tens
|
|
||||||
(10, RoundingIncrement { multiple: MultiplesOf1, magnitude_offset: 1 }), |
|
||||||
(20, RoundingIncrement { multiple: MultiplesOf2, magnitude_offset: 1 }), |
|
||||||
(25, RoundingIncrement { multiple: MultiplesOf25, magnitude_offset: 0 }), |
|
||||||
(50, RoundingIncrement { multiple: MultiplesOf5, magnitude_offset: 1 }), |
|
||||||
// Hundreds
|
|
||||||
(100, RoundingIncrement { multiple: MultiplesOf1, magnitude_offset: 2 }), |
|
||||||
(200, RoundingIncrement { multiple: MultiplesOf2, magnitude_offset: 2 }), |
|
||||||
(250, RoundingIncrement { multiple: MultiplesOf25, magnitude_offset: 1 }), |
|
||||||
(500, RoundingIncrement { multiple: MultiplesOf5, magnitude_offset: 2 }), |
|
||||||
// Thousands
|
|
||||||
(1000, RoundingIncrement { multiple: MultiplesOf1, magnitude_offset: 3 }), |
|
||||||
(2000, RoundingIncrement { multiple: MultiplesOf2, magnitude_offset: 3 }), |
|
||||||
(2500, RoundingIncrement { multiple: MultiplesOf25, magnitude_offset: 2 }), |
|
||||||
(5000, RoundingIncrement { multiple: MultiplesOf5, magnitude_offset: 3 }), |
|
||||||
]; |
|
||||||
|
|
||||||
for (num, increment) in VALID_CASES { |
|
||||||
assert_eq!(RoundingIncrement::from_u16(num), Some(increment)); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn u16_to_rounding_increment_rainy_day() { |
|
||||||
const INVALID_CASES: [u16; 9] = [0, 4, 6, 24, 10000, 65535, 7373, 140, 1500]; |
|
||||||
|
|
||||||
for num in INVALID_CASES { |
|
||||||
assert!(RoundingIncrement::from_u16(num).is_none()); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
Loading…
Reference in new issue