-
-
Notifications
You must be signed in to change notification settings - Fork 616
fix(intl): implement percent style for Intl.NumberFormat and toLocaleString (#5246) #5248
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,203 @@ | ||
| # Locale and Decimal Usage Patterns in BOA INTL | ||
|
|
||
| ## 1. **Getting Language Identifier from `icu_locale::Locale`** | ||
|
|
||
| ### Method: `language()` | ||
| The primary method to extract the language identifier from a `Locale` object: | ||
|
|
||
| ```rust | ||
| // From: core/engine/src/builtins/intl/number_format/mod.rs:82 | ||
| let lang = self.locale.language().as_str(); | ||
| ``` | ||
|
|
||
| This returns the language code as a string slice. Used in `get_percent_symbol()` to determine locale-specific formatting. | ||
|
|
||
| ### Related Locale Methods (Observed Patterns) | ||
| - `locale.language()` - Gets language identifier | ||
| - `locale.to_string()` - Converts locale to full string representation | ||
| - Methods for accessing individual locale components are used but manipulation is typically done through canonicalization | ||
|
|
||
| ### Imports Pattern | ||
| ```rust | ||
| use icu_locale::{Locale, extensions::unicode::Value}; | ||
| use icu_locale::{LanguageIdentifier, Locale, LocaleCanonicalizer}; | ||
| ``` | ||
|
|
||
| The `LanguageIdentifier` is also available, but `Locale` is preferred for full locale information. | ||
|
|
||
| --- | ||
|
|
||
| ## 2. **Decimal from `fixed_decimal` - Manipulation Patterns** | ||
|
|
||
| ### Creation Methods | ||
| ```rust | ||
| // From f64 with precision handling | ||
| Decimal::try_from_f64(x, FloatPrecision::RoundTrip) | ||
|
|
||
| // From string | ||
| Decimal::try_from_str(&s).ok() | ||
|
|
||
| // From BigInt string representation | ||
| Decimal::try_from_str(&bi.to_string()) | ||
|
|
||
| // From integer constant | ||
| Decimal::from(100u32) // For percent multiplication | ||
| Decimal::from(0) // Zero value | ||
| ``` | ||
|
|
||
| ### Arithmetic Operations | ||
| ```rust | ||
| // Multiplication (e.g., for percent conversion) | ||
| // From: core/engine/src/builtins/intl/number_format/mod.rs:532 | ||
| x = x * Decimal::from(100u32); | ||
| ``` | ||
|
|
||
| ### Key Methods on Decimal | ||
| ```rust | ||
| // Formatting operations | ||
| number.round_with_mode_and_increment(position, mode, multiple); | ||
| number.trim_end(); | ||
| number.pad_end(min_msb); | ||
| number.trim_end_if_integer(); | ||
| number.pad_start(i16::from(self.minimum_integer_digits)); | ||
|
|
||
| // Magnitude/Exponent queries | ||
| number.nonzero_magnitude_start() // Get MSB position | ||
| number.magnitude_range().end() // Get magnitude end (for compact notation) | ||
|
|
||
| // Sign operations | ||
| number.apply_sign_display(self.sign_display); | ||
| ``` | ||
|
|
||
| ### CompactDecimal Construction | ||
| ```rust | ||
| // From: core/engine/src/builtins/intl/plural_rules/mod.rs:493 | ||
| let exp = (*fixed.magnitude_range().end()).max(0) as u8; | ||
| let compact = CompactDecimal::from_significand_and_exponent(fixed.clone(), exp); | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## 3. **Imports and Module Organization** | ||
|
|
||
| ### Number Format Imports | ||
| ```rust | ||
| use fixed_decimal::{Decimal, FloatPrecision, SignDisplay}; | ||
| use fixed_decimal::{ | ||
| Decimal, FloatPrecision, RoundingIncrement as BaseMultiple, SignDisplay, SignedRoundingMode, | ||
| UnsignedRoundingMode, | ||
| }; | ||
|
|
||
| use icu_decimal::{ | ||
| DecimalFormatter, DecimalFormatterPreferences, FormattedDecimal, | ||
| options::{DecimalFormatterOptions, GroupingStrategy}, | ||
| preferences::NumberingSystem, | ||
| provider::{DecimalDigitsV1, DecimalSymbolsV1}, | ||
| }; | ||
|
|
||
| use icu_locale::{Locale, extensions::unicode::Value}; | ||
| ``` | ||
|
|
||
| ### Plural Rules Imports | ||
| ```rust | ||
| use fixed_decimal::{CompactDecimal, Decimal, SignedRoundingMode, UnsignedRoundingMode}; | ||
| use icu_locale::Locale; | ||
| ``` | ||
|
|
||
| ### Locale Utilities | ||
| ```rust | ||
| use icu_locale::{LanguageIdentifier, Locale, LocaleCanonicalizer}; | ||
| use icu_locale::extensions::unicode::value; | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## 4. **Common Usage Patterns in `core/engine/src/builtins/intl/`** | ||
|
|
||
| ### Pattern 1: Locale Resolution | ||
| ```rust | ||
| // From locale/utils.rs | ||
| let locale = resolve_locale::<Self>( | ||
| requested_locales, | ||
| &mut intl_options, | ||
| context.intl_provider(), | ||
| )?; | ||
| ``` | ||
|
|
||
| ### Pattern 2: Decimal Formatting with Sign Display | ||
| ```rust | ||
| // From number_format/mod.rs:74-75 | ||
| self.digit_options.format_fixed_decimal(value); | ||
| value.apply_sign_display(self.sign_display); | ||
| self.formatter.format(value) | ||
| ``` | ||
|
|
||
| ### Pattern 3: Percent Formatting | ||
| ```rust | ||
| // From number_format/mod.rs:526-532 | ||
| let is_percent = nf_data.unit_options.style() == Style::Percent; | ||
|
|
||
| if is_percent { | ||
| x = x * Decimal::from(100u32); | ||
| } | ||
| // ... formatting happens | ||
| if is_percent { | ||
| format!("{}{}", formatted, nf_data.get_percent_symbol()) | ||
| } | ||
| ``` | ||
|
|
||
| ### Pattern 4: Compact Notation with Exponent | ||
| ```rust | ||
| // From plural_rules/mod.rs:493-495 | ||
| let exp = (*fixed.magnitude_range().end()).max(0) as u8; | ||
| let compact = CompactDecimal::from_significand_and_exponent(fixed.clone(), exp); | ||
| plural_rules.native.rules().category_for(&compact) | ||
| ``` | ||
|
|
||
| ### Pattern 5: Decimal Construction from Numbers | ||
| ```rust | ||
| // From number_format/options.rs:932 | ||
| let mut number = Decimal::try_from_f64(number, FloatPrecision::RoundTrip) | ||
| .expect("`number` must be finite"); | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## 5. **Error Handling Patterns** | ||
|
|
||
| ### Decimal Parsing Errors | ||
| ```rust | ||
| Decimal::try_from_str(&s) | ||
| .map_err(|err| JsNativeError::range() | ||
| .with_message(err.to_string()).into()) | ||
| ``` | ||
|
|
||
| ### Float Conversion | ||
| ```rust | ||
| Decimal::try_from_f64(x, FloatPrecision::RoundTrip) | ||
| .map_err(|err| JsNativeError::range() | ||
| .with_message(err.to_string()).into()) | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## 6. **File Locations for Reference** | ||
|
|
||
| | File | Purpose | | ||
| |------|---------| | ||
| | `core/engine/src/builtins/intl/number_format/mod.rs` | NumberFormat class, locale language access, percent symbol lookup, Decimal multiplication | | ||
| | `core/engine/src/builtins/intl/number_format/options.rs` | DigitFormatOptions, Decimal rounding & formatting, FixedDecimal API usage | | ||
| | `core/engine/src/builtins/intl/plural_rules/mod.rs` | CompactDecimal construction, magnitude/exponent handling | | ||
| | `core/engine/src/builtins/intl/locale/utils.rs` | Locale resolution, canonicalization, language identifier extraction | | ||
| | `core/engine/src/builtins/intl/` | General INTL module structure with Service trait usage | | ||
|
|
||
| --- | ||
|
|
||
| ## Summary | ||
|
|
||
| - **Locale Language Access**: Use `locale.language().as_str()` to get the language ID as a string | ||
| - **Decimal Creation**: Prefer `try_from_f64()` for numbers or `try_from_str()` for strings | ||
| - **Decimal Arithmetic**: Simple operations via operator overloading (e.g., `*` for multiplication) | ||
| - **Decimal Formatting**: Use methods like `round_with_mode_and_increment()`, `trim_end()`, `pad_start()` | ||
| - **Exponent Access**: Use `magnitude_range()` to get exponent information for compact notation | ||
| - **Compact Decimal**: Use `CompactDecimal::from_significand_and_exponent()` with magnitude data |
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what is this |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -68,14 +68,91 @@ impl NumberFormat { | |
| /// [full]: https://tc39.es/ecma402/#sec-formatnumber | ||
| /// [parts]: https://tc39.es/ecma402/#sec-formatnumbertoparts | ||
| pub(crate) fn format<'a>(&'a self, value: &'a mut Decimal) -> FormattedDecimal<'a> { | ||
| // TODO: Missing support from ICU4X for Percent/Currency/Unit formatting. | ||
| // TODO: Missing support from ICU4X for Currency/Unit formatting. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. as the comment says, the issue is with icu4x (tracked by #3710 in boa and in icu4x by unicode-org/icu4x#4483) so i dont think workarounds in boa are a good idea |
||
| // 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) | ||
| } | ||
|
|
||
| /// Formats a value according to this number format and returns the final display string. | ||
| /// | ||
| /// This currently implements percent style handling in the shared formatting path | ||
| /// so it applies to `Intl.NumberFormat#format`, `Number#toLocaleString` and | ||
| /// `BigInt#toLocaleString`. | ||
| pub(crate) fn format_to_string(&self, value: &mut Decimal) -> String { | ||
| let is_percent = self.unit_options.style() == Style::Percent; | ||
|
|
||
| // Multiply by 100 for percent style before digit formatting, following ECMA-402. | ||
| if is_percent { | ||
| let scaled = Self::scale_decimal_string_by_100(&value.to_string()); | ||
| if let Ok(scaled) = Decimal::try_from_str(&scaled) { | ||
| *value = scaled; | ||
| } | ||
| } | ||
|
|
||
| let formatted = self.format(value).to_string(); | ||
|
|
||
| if is_percent { | ||
| format!("{}{}", formatted, self.get_percent_symbol()) | ||
| } else { | ||
| formatted | ||
| } | ||
| } | ||
|
|
||
| /// Multiply a decimal string by 100 by shifting the decimal point 2 places right. | ||
| fn scale_decimal_string_by_100(input: &str) -> String { | ||
| let (sign, body) = match input.as_bytes().first().copied() { | ||
| Some(b'+') | Some(b'-') => (&input[..1], &input[1..]), | ||
| _ => ("", input), | ||
| }; | ||
|
|
||
| let mut out = if let Some(dot_pos) = body.find('.') { | ||
| let mut digits = body.replace('.', ""); | ||
| let target_pos = dot_pos + 2; | ||
|
|
||
| if target_pos >= digits.len() { | ||
| digits.push_str(&"0".repeat(target_pos - digits.len())); | ||
| digits | ||
| } else { | ||
| digits.insert(target_pos, '.'); | ||
| digits | ||
| } | ||
| } else { | ||
| format!("{body}00") | ||
| }; | ||
|
|
||
| // Normalize trailing fractional zeroes produced after shifting. | ||
| if let Some(dot_pos) = out.find('.') { | ||
| while out.ends_with('0') { | ||
| out.pop(); | ||
| } | ||
| if out.len() == dot_pos + 1 { | ||
| out.pop(); | ||
| } | ||
| } | ||
|
|
||
| if out.is_empty() { | ||
| format!("{sign}0") | ||
| } else { | ||
| format!("{sign}{out}") | ||
| } | ||
| } | ||
|
|
||
| /// Returns the locale-specific percent symbol for this number format. | ||
| fn get_percent_symbol(&self) -> &'static str { | ||
| let locale_str = self.locale.to_string(); | ||
| let lang = locale_str.split('-').next().unwrap_or(""); | ||
| // Most European and Asian locales use a non-breaking space before `%`. | ||
| match lang { | ||
| "de" | "fr" | "es" | "it" | "pt" | "pl" | "nl" | "sv" | "no" | "da" | "fi" | "hu" | ||
| | "cs" | "sk" | "ro" | "bg" | "hr" | "et" | "lt" | "lv" | "sl" | "tr" | "el" | ||
| | "ja" | "ko" | "ru" | "uk" | "be" | "sr" | "mk" => "\u{00A0}%", | ||
| _ => "%", | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl Service for NumberFormat { | ||
|
|
@@ -512,7 +589,7 @@ impl NumberFormat { | |
| 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()) | ||
| Ok(js_string!(nf.borrow().data().format_to_string(&mut x)).into()) | ||
| }, | ||
| nf_clone, | ||
| ), | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
docs are nice but this is hardly the place.