Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 203 additions & 0 deletions LOCALE_DECIMAL_PATTERNS.md
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
Binary file added build_output.txt
Binary file not shown.
2 changes: 1 addition & 1 deletion core/engine/src/builtins/bigint/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ impl BigInt {
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?;

// 3. Return FormatNumeric(numberFormat, ℝ(x)).
Ok(js_string!(number_format.format(x).to_string()).into())
Ok(js_string!(number_format.format_to_string(x)).into())
}

#[cfg(not(feature = "intl"))]
Expand Down
81 changes: 79 additions & 2 deletions core/engine/src/builtins/intl/number_format/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
// 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 {
Expand Down Expand Up @@ -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,
),
Expand Down
27 changes: 27 additions & 0 deletions core/engine/src/builtins/intl/number_format/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,30 @@ fn u16_to_rounding_increment_rainy_day() {
assert!(RoundingIncrement::from_u16(num).is_none());
}
}

#[test]
fn percent_symbol_logic() {
// Test that the percent symbol logic correctly maps locales
let test_cases = vec![
("de", "\u{00A0}%"),
("en", "%"),
("fr", "\u{00A0}%"),
("es", "\u{00A0}%"),
("pt", "\u{00A0}%"),
("ja", "\u{00A0}%"),
("zh", "%"),
("ar", "%"),
];

for (lang, expected_symbol) in test_cases {
let locale_str = format!("{}-XX", lang);
let lang_part = locale_str.split('-').next().unwrap_or("");
let symbol = match lang_part {
"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}%",
_ => "%",
};
assert_eq!(symbol, expected_symbol, "Symbol mismatch for language {}", lang);
}
}
2 changes: 1 addition & 1 deletion core/engine/src/builtins/number/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ impl Number {
.map_err(|err| JsNativeError::range().with_message(err.to_string()))?;

// 3. Return FormatNumeric(numberFormat, ! ToIntlMathematicalValue(x)).
Ok(js_string!(number_format.format(&mut x).to_string()).into())
Ok(js_string!(number_format.format_to_string(&mut x)).into())
}

#[cfg(not(feature = "intl"))]
Expand Down
Loading