-
Notifications
You must be signed in to change notification settings - Fork 183
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement Short&Narrow Compact Currency Formatter (#5450)
- Loading branch information
Showing
4 changed files
with
405 additions
and
0 deletions.
There are no files selected for viewing
162 changes: 162 additions & 0 deletions
162
components/experimental/src/dimension/currency/compact_format.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
202
components/experimental/src/dimension/currency/compact_formatter.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} | ||
} | ||
} |
Oops, something went wrong.