Skip to content
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

Implement LongCurrencyFormatter for Long Currency Formatting #5351

Merged
merged 18 commits into from
Aug 26, 2024
Merged
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
4 changes: 2 additions & 2 deletions components/experimental/src/dimension/currency/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ use fixed_decimal::FixedDecimal;
use icu_decimal::FixedDecimalFormatter;
use writeable::Writeable;

use crate::dimension::currency::formatter::CurrencyCode;
use crate::dimension::currency::options::CurrencyFormatterOptions;
use crate::dimension::currency::options::Width;
use crate::dimension::currency::CurrencyCode;
use crate::dimension::provider::currency;
use crate::dimension::provider::currency::CurrencyEssentialsV1;

Expand Down Expand Up @@ -78,7 +78,7 @@ mod tests {
use tinystr::*;
use writeable::assert_writeable_eq;

use crate::dimension::currency::formatter::{CurrencyCode, CurrencyFormatter};
use crate::dimension::currency::{formatter::CurrencyFormatter, CurrencyCode};

#[test]
pub fn test_en_us() {
Expand Down
11 changes: 3 additions & 8 deletions components/experimental/src/dimension/currency/formatter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
use fixed_decimal::FixedDecimal;
use icu_decimal::{options::FixedDecimalFormatterOptions, FixedDecimalFormatter};
use icu_provider::prelude::*;
use tinystr::TinyAsciiStr;

use super::super::provider::currency::CurrencyEssentialsV1Marker;
use super::format::FormattedCurrency;
use super::options::CurrencyFormatterOptions;
use super::CurrencyCode;

extern crate alloc;

Expand All @@ -34,10 +34,6 @@ pub struct CurrencyFormatter {
fixed_decimal_formatter: FixedDecimalFormatter,
}

/// A currency code, such as "USD" or "EUR".
#[derive(Clone, Copy)]
pub struct CurrencyCode(pub TinyAsciiStr<3>);

impl CurrencyFormatter {
icu_provider::gen_any_buffer_data_constructors!(
(locale, options: super::options::CurrencyFormatterOptions) -> error: DataError,
Expand Down Expand Up @@ -110,9 +106,8 @@ impl CurrencyFormatter {
///
/// # Examples
/// ```
/// use icu::experimental::dimension::currency::formatter::{
/// CurrencyCode, CurrencyFormatter,
/// };
/// use icu::experimental::dimension::currency::formatter::CurrencyFormatter;
/// use icu::experimental::dimension::currency::CurrencyCode;
/// use icu::locale::locale;
/// use tinystr::*;
/// use writeable::Writeable;
Expand Down
101 changes: 101 additions & 0 deletions components/experimental/src/dimension/currency/long_format.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// 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 fixed_decimal::FixedDecimal;

use icu_decimal::FixedDecimalFormatter;
use icu_plurals::PluralRules;
use writeable::Writeable;

use crate::dimension::provider::currency_patterns::CurrencyPatternsDataV1;
use crate::dimension::provider::extended_currency::CurrencyExtendedDataV1;

use super::CurrencyCode;

pub struct LongFormattedCurrency<'l> {
pub(crate) value: &'l FixedDecimal,
// 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) fixed_decimal_formatter: &'l FixedDecimalFormatter,
pub(crate) plural_rules: &'l PluralRules,
}

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

impl<'l> Writeable for LongFormattedCurrency<'l> {
fn write_to<W>(&self, sink: &mut W) -> core::result::Result<(), core::fmt::Error>
where
W: core::fmt::Write + ?Sized,
{
let plural_category = self.plural_rules.category_for(self.value);
let display_name = self.extended.display_names.get_str(plural_category);
let pattern = self.patterns.patterns.get_pattern(plural_category);
let formatted_value = self.fixed_decimal_formatter.format(self.value);
let interpolated = pattern.interpolate((formatted_value, display_name));
interpolated.write_to(sink)
Comment on lines +33 to +38
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Praise: good code here, just loading things and interpolating them. No allocations.

}
}

// TODO: add more tests for this module to cover more locales & currencies.
#[cfg(test)]
mod tests {
use icu_locale_core::locale;
use tinystr::*;
use writeable::assert_writeable_eq;

use crate::dimension::currency::{long_formatter::LongCurrencyFormatter, CurrencyCode};

#[test]
pub fn test_en_us() {
let locale = locale!("en-US").into();
let currency_code = CurrencyCode(tinystr!(3, "USD"));
let fmt = LongCurrencyFormatter::try_new(&locale, &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,345.67 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,345.67 US dollars");
}

#[test]
pub fn test_fr_fr() {
let locale = locale!("fr-FR").into();
let currency_code = CurrencyCode(tinystr!(3, "EUR"));
let fmt = LongCurrencyFormatter::try_new(&locale, &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\u{202f}345,67 euros");

// 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{202f}345,67 euros");
}

#[test]
pub fn test_ar_eg() {
let locale = locale!("ar-EG").into();
let currency_code = CurrencyCode(tinystr!(3, "EGP"));
let fmt = LongCurrencyFormatter::try_new(&locale, &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, "١٢٬٣٤٥٫٦٧ جنيه مصري");

// 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{61c}-١٢٬٣٤٥٫٦٧ جنيه مصري");
}
}
173 changes: 173 additions & 0 deletions components/experimental/src/dimension/currency/long_formatter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// 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::FixedDecimal;
use icu_decimal::{options::FixedDecimalFormatterOptions, FixedDecimalFormatter};
use icu_plurals::PluralRules;
use icu_provider::prelude::*;

use crate::dimension::provider::{
currency_patterns::CurrencyPatternsDataV1Marker,
extended_currency::CurrencyExtendedDataV1Marker,
};

use super::{long_format::LongFormattedCurrency, CurrencyCode};

extern crate alloc;

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

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

/// A [`FixedDecimalFormatter`] to format the currency value.
fixed_decimal_formatter: FixedDecimalFormatter,

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

impl LongCurrencyFormatter {
icu_provider::gen_any_buffer_data_constructors!(
(locale: &DataLocale, 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 [`LongCurrencyFormatter`] 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(locale: &DataLocale, currency_code: &CurrencyCode) -> Result<Self, DataError> {
let fixed_decimal_formatter =
FixedDecimalFormatter::try_new(locale, FixedDecimalFormatterOptions::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 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(locale)?;

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

#[doc = icu_provider::gen_any_buffer_unstable_docs!(UNSTABLE, Self::try_new)]
pub fn try_new_unstable<D>(
provider: &D,
locale: &DataLocale,
currency_code: &CurrencyCode,
) -> Result<Self, DataError>
where
D: ?Sized
+ DataProvider<super::super::provider::extended_currency::CurrencyExtendedDataV1Marker>
+ DataProvider<super::super::provider::currency_patterns::CurrencyPatternsDataV1Marker>
+ DataProvider<icu_decimal::provider::DecimalSymbolsV1Marker>
+ DataProvider<icu_plurals::provider::CardinalV1Marker>,
{
let fixed_decimal_formatter = FixedDecimalFormatter::try_new_unstable(
provider,
locale,
FixedDecimalFormatterOptions::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 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, locale)?;

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

/// Formats in the long format a [`FixedDecimal`] value for the given currency code.
///
/// # Examples
/// ```
/// use icu::experimental::dimension::currency::long_formatter::LongCurrencyFormatter;
/// 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 = LongCurrencyFormatter::try_new(&locale, &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,345.67 US dollars");
/// ```
pub fn format_fixed_decimal<'l>(
&'l self,
value: &'l FixedDecimal,
currency_code: CurrencyCode,
) -> LongFormattedCurrency<'l> {
LongFormattedCurrency {
value,
_currency_code: currency_code,
extended: self.extended.get(),
patterns: self.patterns.get(),
fixed_decimal_formatter: &self.fixed_decimal_formatter,
plural_rules: &self.plural_rules,
}
}
}
8 changes: 8 additions & 0 deletions components/experimental/src/dimension/currency/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@

//! Experimental.

use tinystr::TinyAsciiStr;

pub mod format;
pub mod formatter;
pub mod long_format;
pub mod long_formatter;
pub mod options;

/// A currency code, such as "USD" or "EUR".
#[derive(Clone, Copy)]
pub struct CurrencyCode(pub TinyAsciiStr<3>);
6 changes: 6 additions & 0 deletions provider/source/src/currency/patterns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ impl DataProvider<CurrencyPatternsDataV1Marker> for SourceDataProvider {
.get(default_system)
.ok_or(DataErrorKind::IdentifierNotFound.into_error())?;

// According to [Unicode Technical Standard #35](https://unicode.org/reports/tr35/tr35-numbers.html),
// The `other` pattern must be present in the data.
if patterns.pattern_other.is_none() {
return Err(DataErrorKind::IdentifierNotFound.into_error());
}

Ok(DataResponse {
metadata: Default::default(),
payload: DataPayload::from_owned(CurrencyPatternsDataV1 {
Expand Down