Skip to content

Commit

Permalink
Implement Short&Narrow Compact Currency Formatter (#5450)
Browse files Browse the repository at this point in the history
  • Loading branch information
younies authored Dec 11, 2024
1 parent 1823594 commit b21d65a
Show file tree
Hide file tree
Showing 4 changed files with 405 additions and 0 deletions.
162 changes: 162 additions & 0 deletions components/experimental/src/dimension/currency/compact_format.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// 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::{
compact_options::{CompactCurrencyFormatterOptions, Width},
CurrencyCode,
};
use crate::{
compactdecimal::CompactDecimalFormatter,
dimension::provider::{
currency::{self, CurrencyEssentialsV1},
currency_compact::ShortCurrencyCompactV1,
},
};
use fixed_decimal::SignedFixedDecimal;
use writeable::Writeable;

pub struct FormattedCompactCurrency<'l> {
pub(crate) value: &'l SignedFixedDecimal,
pub(crate) currency_code: CurrencyCode,
pub(crate) options: &'l CompactCurrencyFormatterOptions,
pub(crate) essential: &'l CurrencyEssentialsV1<'l>,
pub(crate) _short_currency_compact: &'l ShortCurrencyCompactV1<'l>,
pub(crate) compact_decimal_formatter: &'l CompactDecimalFormatter,
}

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

impl<'l> Writeable for FormattedCompactCurrency<'l> {
fn write_to<W>(&self, sink: &mut W) -> core::result::Result<(), core::fmt::Error>
where
W: core::fmt::Write + ?Sized,
{
let config = self
.essential
.pattern_config_map
.get_copied(&self.currency_code.0.to_unvalidated())
.unwrap_or(self.essential.default_pattern_config);

let placeholder_index = match self.options.width {
Width::Short => config.short_placeholder_value,
Width::Narrow => config.narrow_placeholder_value,
};

let currency_placeholder = match placeholder_index {
Some(currency::PlaceholderValue::Index(index)) => self
.essential
.placeholders
.get(index.into())
.ok_or(core::fmt::Error)?,
Some(currency::PlaceholderValue::ISO) | None => self.currency_code.0.as_str(),
};

let pattern_selection = match self.options.width {
Width::Short => config.short_pattern_selection,
Width::Narrow => config.narrow_pattern_selection,
};

// TODO: The current behavior is the behavior when there is no compact currency pattern found.
// Therefore, in the next PR, we will add the code to handle using the compact currency patterns.

let pattern = match pattern_selection {
currency::PatternSelection::Standard => self.essential.standard_pattern.as_ref(),
currency::PatternSelection::StandardAlphaNextToNumber => self
.essential
.standard_alpha_next_to_number_pattern
.as_ref(),
}
.ok_or(core::fmt::Error)?;

pattern
.interpolate((
self.compact_decimal_formatter
.format_fixed_decimal(self.value),
currency_placeholder,
))
.write_to(sink)?;

Ok(())
}
}

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

use crate::dimension::currency::{compact_formatter::CompactCurrencyFormatter, CurrencyCode};

#[test]
pub fn test_en_us() {
let locale = locale!("en-US").into();
let currency_code = CurrencyCode(tinystr!(3, "USD"));
let fmt = CompactCurrencyFormatter::try_new(locale, Default::default()).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, "$12K");

// 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, "$-12K");
}

#[test]
pub fn test_fr_fr() {
let locale = locale!("fr-FR").into();
let currency_code = CurrencyCode(tinystr!(3, "EUR"));
let fmt = CompactCurrencyFormatter::try_new(locale, Default::default()).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\u{a0}k\u{a0}€");

// 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\u{a0}k\u{a0}€");
}

#[test]
pub fn test_zh_cn() {
let locale = locale!("zh-CN").into();
let currency_code = CurrencyCode(tinystr!(3, "CNY"));
let fmt = CompactCurrencyFormatter::try_new(locale, Default::default()).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, "¥1.2万");

// 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, "¥-1.2万");
}

#[test]
pub fn test_ar_eg() {
let locale = locale!("ar-EG").into();
let currency_code = CurrencyCode(tinystr!(3, "EGP"));
let fmt = CompactCurrencyFormatter::try_new(locale, Default::default()).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, "\u{200f}١٢\u{a0}ألف\u{a0}ج.م.\u{200f}"); // "١٢ ألف ج.م."

// 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,
"\u{200f}\u{61c}-١٢\u{a0}ألف\u{a0}ج.م.\u{200f}"
);
}
}
202 changes: 202 additions & 0 deletions components/experimental/src/dimension/currency/compact_formatter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
// 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 crate::{
compactdecimal::{
CompactDecimalFormatter, CompactDecimalFormatterOptions, CompactDecimalFormatterPreferences,
},
dimension::provider::{
currency::CurrencyEssentialsV1Marker, currency_compact::ShortCurrencyCompactV1Marker,
},
};
use fixed_decimal::SignedFixedDecimal;
use icu_decimal::FixedDecimalFormatterPreferences;
use icu_locale_core::preferences::{
define_preferences, extensions::unicode::keywords::NumberingSystem, prefs_convert,
};
use icu_provider::prelude::*;

use super::{
compact_format::FormattedCompactCurrency, compact_options::CompactCurrencyFormatterOptions,
CurrencyCode,
};

extern crate alloc;

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

prefs_convert!(
CompactCurrencyFormatterPreferences,
FixedDecimalFormatterPreferences,
{ numbering_system }
);
prefs_convert!(
CompactCurrencyFormatterPreferences,
CompactDecimalFormatterPreferences
);

/// A formatter for monetary values.
///
/// [`CompactCurrencyFormatter`] supports:
/// 1. Rendering in the locale's currency system.
/// 2. Locale-sensitive grouping separator positions.
///
/// Read more about the options in the [`super::compact_options`] module.
pub struct CompactCurrencyFormatter {
/// Short currency compact data for the compact currency formatter.
short_currency_compact: DataPayload<ShortCurrencyCompactV1Marker>,

/// Essential data for the compact currency formatter.
essential: DataPayload<CurrencyEssentialsV1Marker>,

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

/// Options bag for the compact currency formatter to determine the behavior of the formatter.
/// for example: width.
options: CompactCurrencyFormatterOptions,
}

impl CompactCurrencyFormatter {
icu_provider::gen_any_buffer_data_constructors!(
(prefs: CompactCurrencyFormatterPreferences, options: CompactCurrencyFormatterOptions) -> error: DataError,
functions: [
try_new: skip,
try_new_with_any_provider,
try_new_with_buffer_provider,
try_new_unstable,
Self
]
);

/// Creates a new [`CompactCurrencyFormatter`] from compiled locale data and an options bag.
///
/// ✨ *Enabled with the `compiled_data` Cargo feature.*
///
/// [📚 Help choosing a constructor](icu_provider::constructors)
#[cfg(feature = "compiled_data")]
pub fn try_new(
prefs: CompactCurrencyFormatterPreferences,
options: CompactCurrencyFormatterOptions,
) -> Result<Self, DataError> {
let short_locale =
DataLocale::from_preferences_locale::<ShortCurrencyCompactV1Marker>(prefs.locale_prefs);

let short_currency_compact = crate::provider::Baked
.load(DataRequest {
id: DataIdentifierBorrowed::for_locale(&short_locale),
..Default::default()
})?
.payload;

let essential_locale =
DataLocale::from_preferences_locale::<CurrencyEssentialsV1Marker>(prefs.locale_prefs);

let essential = crate::provider::Baked
.load(DataRequest {
id: DataIdentifierBorrowed::for_locale(&essential_locale),
..Default::default()
})?
.payload;

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

Ok(Self {
short_currency_compact,
essential,
compact_decimal_formatter,
options,
})
}

#[doc = icu_provider::gen_any_buffer_unstable_docs!(UNSTABLE, Self::try_new)]
pub fn try_new_unstable<D>(
provider: &D,
prefs: CompactCurrencyFormatterPreferences,
options: CompactCurrencyFormatterOptions,
) -> Result<Self, DataError>
where
D: ?Sized
+ DataProvider<crate::dimension::provider::currency::CurrencyEssentialsV1Marker>
+ DataProvider<crate::dimension::provider::currency_compact::ShortCurrencyCompactV1Marker>
+ DataProvider<crate::compactdecimal::provider::ShortCompactDecimalFormatDataV1Marker>
+ DataProvider<icu_decimal::provider::DecimalSymbolsV2Marker>
+ DataProvider<icu_decimal::provider::DecimalDigitsV1Marker>
+ DataProvider<icu_plurals::provider::CardinalV1Marker>,
{
let locale =
DataLocale::from_preferences_locale::<CurrencyEssentialsV1Marker>(prefs.locale_prefs);

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

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

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

Ok(Self {
short_currency_compact,
essential,
compact_decimal_formatter,
options,
})
}

/// Formats in the compact format a [`SignedFixedDecimal`] value for the given currency code.
///
/// # Examples
/// ```
/// use icu::experimental::dimension::currency::compact_formatter::CompactCurrencyFormatter;
/// use icu::experimental::dimension::currency::CurrencyCode;
/// use icu::locale::locale;
/// use tinystr::*;
/// use writeable::Writeable;
///
/// let locale = locale!("en-US").into();
/// let currency_code = CurrencyCode(tinystr!(3, "USD"));
/// let fmt = CompactCurrencyFormatter::try_new(locale, Default::default()).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(), "$12K");
/// ```
pub fn format_fixed_decimal<'l>(
&'l self,
value: &'l SignedFixedDecimal,
currency_code: CurrencyCode,
) -> FormattedCompactCurrency<'l> {
FormattedCompactCurrency {
value,
currency_code,
options: &self.options,
essential: self.essential.get(),
_short_currency_compact: self.short_currency_compact.get(),
compact_decimal_formatter: &self.compact_decimal_formatter,
}
}
}
Loading

0 comments on commit b21d65a

Please sign in to comment.