Skip to content

Commit

Permalink
Implement Long Compact Currency Formatter (#5456)
Browse files Browse the repository at this point in the history
  • Loading branch information
younies authored Dec 11, 2024
1 parent 8717dfc commit 1823594
Show file tree
Hide file tree
Showing 3 changed files with 346 additions and 0 deletions.
127 changes: 127 additions & 0 deletions components/experimental/src/dimension/currency/long_compact_format.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// This file is part of ICU4X. For terms of use, please see the file
// called LICENSE at the top level of the ICU4X source tree
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).

use super::CurrencyCode;
use crate::compactdecimal::CompactDecimalFormatter;
use crate::dimension::provider::currency_patterns::CurrencyPatternsDataV1;
use crate::dimension::provider::extended_currency::CurrencyExtendedDataV1;
use fixed_decimal::SignedFixedDecimal;
use icu_plurals::PluralRules;
use writeable::Writeable;

pub struct FormattedLongCompactCurrency<'l> {
pub(crate) signed_fixed_decimal: &'l SignedFixedDecimal,
// TODO: use this if the display name is not exist and make the extended data optional.
pub(crate) _currency_code: CurrencyCode,
pub(crate) extended: &'l CurrencyExtendedDataV1<'l>,
pub(crate) patterns: &'l CurrencyPatternsDataV1<'l>,
pub(crate) compact_decimal_formatter: &'l CompactDecimalFormatter,
pub(crate) plural_rules: &'l PluralRules,
}

writeable::impl_display_with_writeable!(FormattedLongCompactCurrency<'_>);

impl Writeable for FormattedLongCompactCurrency<'_> {
fn write_to<W>(&self, sink: &mut W) -> core::result::Result<(), core::fmt::Error>
where
W: core::fmt::Write + ?Sized,
{
let operands = self.signed_fixed_decimal.into();

let display_name = self.extended.display_names.get(operands, self.plural_rules);
let pattern = self.patterns.patterns.get(operands, self.plural_rules);

let formatted_value = self
.compact_decimal_formatter
.format_fixed_decimal(self.signed_fixed_decimal);
let interpolated = pattern.interpolate((formatted_value, display_name));
interpolated.write_to(sink)
}
}

#[cfg(test)]
mod tests {
use icu_locale_core::locale;
use tinystr::*;
use writeable::assert_writeable_eq;

use crate::dimension::currency::long_compact_formatter::LongCompactCurrencyFormatter;
use crate::dimension::currency::CurrencyCode;

#[test]
pub fn test_en_us() {
let currency_formatter_prefs = locale!("en-US").into();

let currency_code = CurrencyCode(tinystr!(3, "USD"));
let fmt = LongCompactCurrencyFormatter::try_new(currency_formatter_prefs, &currency_code)
.unwrap();

// Positive case
let positive_value = "12345.67".parse().unwrap();
let formatted_currency = fmt.format_fixed_decimal(&positive_value, currency_code);
assert_writeable_eq!(formatted_currency, "12 thousand US dollars");

// Negative case
let negative_value = "-12345.67".parse().unwrap();
let formatted_currency = fmt.format_fixed_decimal(&negative_value, currency_code);
assert_writeable_eq!(formatted_currency, "-12 thousand US dollars");
}

#[test]
pub fn test_en_us_millions() {
let currency_formatter_prefs = locale!("en-US").into();

let currency_code = CurrencyCode(tinystr!(3, "USD"));
let fmt = LongCompactCurrencyFormatter::try_new(currency_formatter_prefs, &currency_code)
.unwrap();

// Positive case
let positive_value = "12345000.67".parse().unwrap();
let formatted_currency = fmt.format_fixed_decimal(&positive_value, currency_code);
assert_writeable_eq!(formatted_currency, "12 million US dollars");

// Negative case
let negative_value = "-12345000.67".parse().unwrap();
let formatted_currency = fmt.format_fixed_decimal(&negative_value, currency_code);
assert_writeable_eq!(formatted_currency, "-12 million US dollars");
}

#[test]
pub fn test_fr_fr() {
let currency_formatter_prefs = locale!("fr-FR").into();

let currency_code = CurrencyCode(tinystr!(3, "USD"));
let fmt = LongCompactCurrencyFormatter::try_new(currency_formatter_prefs, &currency_code)
.unwrap();

// Positive case
let positive_value = "12345.67".parse().unwrap();
let formatted_currency = fmt.format_fixed_decimal(&positive_value, currency_code);
assert_writeable_eq!(formatted_currency, "12 mille dollars des États-Unis");

// Negative case
let negative_value = "-12345.67".parse().unwrap();
let formatted_currency = fmt.format_fixed_decimal(&negative_value, currency_code);
assert_writeable_eq!(formatted_currency, "-12 mille dollars des États-Unis");
}

#[test]
pub fn test_fr_fr_millions() {
let currency_formatter_prefs = locale!("fr-FR").into();

let currency_code = CurrencyCode(tinystr!(3, "USD"));
let fmt = LongCompactCurrencyFormatter::try_new(currency_formatter_prefs, &currency_code)
.unwrap();

// Positive case
let positive_value = "12345000.67".parse().unwrap();
let formatted_currency = fmt.format_fixed_decimal(&positive_value, currency_code);
assert_writeable_eq!(formatted_currency, "12 millions dollars des États-Unis");

// Negative case
let negative_value = "-12345000.67".parse().unwrap();
let formatted_currency = fmt.format_fixed_decimal(&negative_value, currency_code);
assert_writeable_eq!(formatted_currency, "-12 millions dollars des États-Unis");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
// This file is part of ICU4X. For terms of use, please see the file
// called LICENSE at the top level of the ICU4X source tree
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).

//! Experimental.
use fixed_decimal::SignedFixedDecimal;
use icu_plurals::{PluralRules, PluralRulesPreferences};
use icu_provider::prelude::*;

use crate::{
compactdecimal::CompactDecimalFormatter,
compactdecimal::CompactDecimalFormatterOptions,
compactdecimal::CompactDecimalFormatterPreferences,
dimension::provider::{
currency_patterns::CurrencyPatternsDataV1Marker,
extended_currency::CurrencyExtendedDataV1Marker,
},
};
use icu_locale_core::preferences::{
define_preferences, extensions::unicode::keywords::NumberingSystem, prefs_convert,
};

use super::{long_compact_format::FormattedLongCompactCurrency, CurrencyCode};

extern crate alloc;

define_preferences!(
/// The preferences for currency formatting.
[Copy]
LongCompactCurrencyFormatterPreferences,
{
numbering_system: NumberingSystem
}
);

prefs_convert!(
LongCompactCurrencyFormatterPreferences,
CompactDecimalFormatterPreferences,
{ numbering_system }
);
prefs_convert!(
LongCompactCurrencyFormatterPreferences,
PluralRulesPreferences
);

/// A formatter for monetary values.
///
/// [`LongCompactCurrencyFormatter`] supports:
/// 1. Rendering in the locale's currency system.
/// 2. Locale-sensitive grouping separator positions.
pub struct LongCompactCurrencyFormatter {
/// Extended data for the currency formatter.
extended: DataPayload<CurrencyExtendedDataV1Marker>,

/// Formatting patterns for each currency plural category.
patterns: DataPayload<CurrencyPatternsDataV1Marker>,

/// A [`CompactDecimalFormatter`] to format the currency value in compact form.
compact_decimal_formatter: CompactDecimalFormatter,

/// A [`PluralRules`] to determine the plural category of the unit.
plural_rules: PluralRules,
}

impl LongCompactCurrencyFormatter {
icu_provider::gen_any_buffer_data_constructors!(
(
prefs: LongCompactCurrencyFormatterPreferences,
currency_code: &CurrencyCode
) -> error: DataError,
functions: [
try_new: skip,
try_new_with_any_provider,
try_new_with_buffer_provider,
try_new_unstable,
Self
]
);

/// Creates a new [`LongCompactCurrencyFormatter`] from compiled locale data.
///
/// ✨ *Enabled with the `compiled_data` Cargo feature.*
///
/// [📚 Help choosing a constructor](icu_provider::constructors)
#[cfg(feature = "compiled_data")]
pub fn try_new(
prefs: LongCompactCurrencyFormatterPreferences,
currency_code: &CurrencyCode,
) -> Result<Self, DataError> {
let compact_decimal_formatter = CompactDecimalFormatter::try_new_long(
(&prefs).into(),
CompactDecimalFormatterOptions::default(),
)?;

let marker_attributes = DataMarkerAttributes::try_from_str(currency_code.0.as_str())
.map_err(|_| {
DataErrorKind::IdentifierNotFound
.into_error()
.with_debug_context("failed to get data marker attribute from a `CurrencyCode`")
})?;

let locale = &DataLocale::from_preferences_locale::<CurrencyPatternsDataV1Marker>(
prefs.locale_prefs,
);

let extended = crate::provider::Baked
.load(DataRequest {
id: DataIdentifierBorrowed::for_marker_attributes_and_locale(
marker_attributes,
locale,
),
..Default::default()
})?
.payload;

let patterns = crate::provider::Baked.load(Default::default())?.payload;

let plural_rules = PluralRules::try_new_cardinal((&prefs).into())?;

Ok(Self {
extended,
patterns,
compact_decimal_formatter,
plural_rules,
})
}

#[doc = icu_provider::gen_any_buffer_unstable_docs!(UNSTABLE, Self::try_new)]
pub fn try_new_unstable<D>(
provider: &D,
prefs: LongCompactCurrencyFormatterPreferences,
currency_code: &CurrencyCode,
) -> Result<Self, DataError>
where
D: ?Sized
+ DataProvider<
crate::dimension::provider::extended_currency::CurrencyExtendedDataV1Marker,
> + DataProvider<
crate::dimension::provider::currency_patterns::CurrencyPatternsDataV1Marker,
> + DataProvider<icu_decimal::provider::DecimalSymbolsV2Marker>
+ DataProvider<icu_decimal::provider::DecimalDigitsV1Marker>
+ DataProvider<icu_plurals::provider::CardinalV1Marker>
+ DataProvider<crate::compactdecimal::provider::LongCompactDecimalFormatDataV1Marker>,
{
let locale =
DataLocale::from_preferences_locale::<CurrencyPatternsDataV1Marker>(prefs.locale_prefs);

let marker_attributes = DataMarkerAttributes::try_from_str(currency_code.0.as_str())
.map_err(|_| {
DataErrorKind::IdentifierNotFound
.into_error()
.with_debug_context("failed to get data marker attribute from a `CurrencyCode`")
})?;

let extended = provider
.load(DataRequest {
id: DataIdentifierBorrowed::for_marker_attributes_and_locale(
marker_attributes,
&locale,
),
..Default::default()
})?
.payload;

let patterns = provider.load(Default::default())?.payload;

let plural_rules = PluralRules::try_new_cardinal_unstable(provider, (&prefs).into())?;

let compact_decimal_formatter = CompactDecimalFormatter::try_new_long_unstable(
provider,
(&prefs).into(),
CompactDecimalFormatterOptions::default(),
)?;

Ok(Self {
extended,
patterns,
compact_decimal_formatter,
plural_rules,
})
}

/// Formats in the long format a [`SignedFixedDecimal`] value for the given currency code.
///
/// # Examples
/// ```
/// use icu::experimental::dimension::currency::long_compact_formatter::LongCompactCurrencyFormatter;
/// use icu::experimental::dimension::currency::CurrencyCode;
/// use icu::locale::locale;
/// use tinystr::*;
/// use writeable::Writeable;
///
/// let currency_prefs = locale!("en-US").into();
/// let currency_code = CurrencyCode(tinystr!(3, "USD"));
/// let fmt = LongCompactCurrencyFormatter::try_new(currency_prefs, &currency_code).unwrap();
/// let value = "12345.67".parse().unwrap();
/// let formatted_currency = fmt.format_fixed_decimal(&value, currency_code);
/// let mut sink = String::new();
/// formatted_currency.write_to(&mut sink).unwrap();
/// assert_eq!(sink.as_str(), "12 thousand US dollars");
/// ```
pub fn format_fixed_decimal<'l>(
&'l self,
value: &'l SignedFixedDecimal,
currency_code: CurrencyCode,
) -> FormattedLongCompactCurrency<'l> {
FormattedLongCompactCurrency {
signed_fixed_decimal: value,
_currency_code: currency_code,
extended: self.extended.get(),
patterns: self.patterns.get(),
compact_decimal_formatter: &self.compact_decimal_formatter,
plural_rules: &self.plural_rules,
}
}
}
2 changes: 2 additions & 0 deletions components/experimental/src/dimension/currency/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ use tinystr::TinyAsciiStr;

pub mod format;
pub mod formatter;
pub mod long_compact_format;
pub mod long_compact_formatter;
pub mod long_format;
pub mod long_formatter;
pub mod options;
Expand Down

0 comments on commit 1823594

Please sign in to comment.